tor-commits
Threads by month
- ----- 2025 -----
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
July 2011
- 16 participants
- 868 discussions
commit adf453c2402b805582d448d17f62ac173f00ec61
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Apr 26 19:20:55 2011 -0700
Renaming interface directory to cli
Kamran's working on an arm gui for GSoC, so renaming the interface directory to
cli (and he'll introduce a /gui).
---
src/cli/__init__.py | 6 +
src/cli/configPanel.py | 364 +++++++
src/cli/connections/__init__.py | 6 +
src/cli/connections/circEntry.py | 216 ++++
src/cli/connections/connEntry.py | 850 ++++++++++++++++
src/cli/connections/connPanel.py | 398 ++++++++
src/cli/connections/entries.py | 164 +++
src/cli/controller.py | 1584 ++++++++++++++++++++++++++++++
src/cli/descriptorPopup.py | 181 ++++
src/cli/graphing/__init__.py | 6 +
src/cli/graphing/bandwidthStats.py | 398 ++++++++
src/cli/graphing/connStats.py | 54 +
src/cli/graphing/graphPanel.py | 407 ++++++++
src/cli/graphing/resourceStats.py | 47 +
src/cli/headerPanel.py | 474 +++++++++
src/cli/logPanel.py | 1100 +++++++++++++++++++++
src/cli/torrcPanel.py | 221 +++++
src/interface/__init__.py | 6 -
src/interface/configPanel.py | 364 -------
src/interface/connections/__init__.py | 6 -
src/interface/connections/circEntry.py | 216 ----
src/interface/connections/connEntry.py | 850 ----------------
src/interface/connections/connPanel.py | 398 --------
src/interface/connections/entries.py | 164 ---
src/interface/controller.py | 1584 ------------------------------
src/interface/descriptorPopup.py | 181 ----
src/interface/graphing/__init__.py | 6 -
src/interface/graphing/bandwidthStats.py | 398 --------
src/interface/graphing/connStats.py | 54 -
src/interface/graphing/graphPanel.py | 407 --------
src/interface/graphing/resourceStats.py | 47 -
src/interface/headerPanel.py | 474 ---------
src/interface/logPanel.py | 1100 ---------------------
src/interface/torrcPanel.py | 221 -----
src/starter.py | 10 +-
35 files changed, 6481 insertions(+), 6481 deletions(-)
diff --git a/src/cli/__init__.py b/src/cli/__init__.py
new file mode 100644
index 0000000..0f11fc1
--- /dev/null
+++ b/src/cli/__init__.py
@@ -0,0 +1,6 @@
+"""
+Panels, popups, and handlers comprising the arm user interface.
+"""
+
+__all__ = ["configPanel", "connPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
+
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
new file mode 100644
index 0000000..fd6fb54
--- /dev/null
+++ b/src/cli/configPanel.py
@@ -0,0 +1,364 @@
+"""
+Panel presenting the configuration state for tor or arm. Options can be edited
+and the resulting configuration files saved.
+"""
+
+import curses
+import threading
+
+from util import conf, enum, panel, torTools, torConfig, uiTools
+
+DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
+ "features.config.state.showPrivateOptions": False,
+ "features.config.state.showVirtualOptions": False,
+ "features.config.state.colWidth.option": 25,
+ "features.config.state.colWidth.value": 15}
+
+# TODO: The arm use cases are incomplete since they currently can't be
+# modified, have their descriptions fetched, or even get a complete listing
+# of what's available.
+State = enum.Enum("TOR", "ARM") # state to be presented
+
+# mappings of option categories to the color for their entries
+CATEGORY_COLOR = {torConfig.Category.GENERAL: "green",
+ torConfig.Category.CLIENT: "blue",
+ torConfig.Category.RELAY: "yellow",
+ torConfig.Category.DIRECTORY: "magenta",
+ torConfig.Category.AUTHORITY: "red",
+ torConfig.Category.HIDDEN_SERVICE: "cyan",
+ torConfig.Category.TESTING: "white",
+ torConfig.Category.UNKNOWN: "white"}
+
+# attributes of a ConfigEntry
+Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE",
+ "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT")
+DEFAULT_SORT_ORDER = (Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT)
+FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
+ Field.OPTION: ("Option Name", "blue"),
+ Field.VALUE: ("Value", "cyan"),
+ Field.TYPE: ("Arg Type", "green"),
+ Field.ARG_USAGE: ("Arg Usage", "yellow"),
+ Field.SUMMARY: ("Summary", "green"),
+ Field.DESCRIPTION: ("Description", "white"),
+ Field.MAN_ENTRY: ("Man Page Entry", "blue"),
+ Field.IS_DEFAULT: ("Is Default", "magenta")}
+
+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
+ self.fields[Field.ARG_USAGE] = manEntry.argUsage
+ self.fields[Field.DESCRIPTION] = manEntry.description
+ else:
+ self.fields[Field.MAN_ENTRY] = 99999 # sorts non-man entries last
+ 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)
+ valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth)
+ summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None)
+ lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth)
+ self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
+ self.labelCacheArgs = argSet
+
+ return self.labelCache
+
+ 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"):
+ confValue = "False" if confValue == "0" else "True"
+ elif self.get(Field.TYPE) == "DataSize" and confValue.isdigit():
+ confValue = uiTools.getSizeLabel(int(confValue))
+ elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit():
+ confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
+
+ return confValue
+
+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, config=None):
+ panel.Panel.__init__(self, stdscr, "configState", 0)
+
+ self.sortOrdering = DEFAULT_SORT_ORDER
+ self._config = dict(DEFAULT_CONFIG)
+ if config:
+ config.update(self._config, {
+ "features.config.selectionDetails.height": 0,
+ "features.config.state.colWidth.option": 5,
+ "features.config.state.colWidth.value": 5})
+
+ sortFields = Field.values()
+ customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields))
+
+ if customOrdering:
+ self.sortOrdering = [sortFields[i] for i in customOrdering]
+
+ self.configType = configType
+ self.confContents = []
+ self.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
+
+ if self.configType == State.TOR:
+ conn = torTools.getConn()
+ customOptions = torConfig.getCustomOptions()
+ configOptionLines = conn.getInfo("config/names", "").strip().split("\n")
+
+ for line in configOptionLines:
+ # lines are of the form "<option> <type>", like:
+ # UseEntryGuards Boolean
+ confOption, confType = line.strip().split(" ", 1)
+
+ # skips private and virtual entries if not configured to show them
+ if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
+ continue
+ elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual":
+ continue
+
+ self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions))
+ elif self.configType == State.ARM:
+ # loaded via the conf utility
+ armConf = conf.getConfig("arm")
+ for key in armConf.getKeys():
+ 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 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: self.sortOrdering = ordering
+ self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
+ self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
+ self.valsLock.release()
+
+ def handleKey(self, key):
+ self.valsLock.acquire()
+ if uiTools.isScrollKey(key):
+ pageHeight = self.getPreferredSize()[0] - 1
+ detailPanelHeight = self._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 key == ord('a') or key == ord('A'):
+ self.showAll = not self.showAll
+ self.redraw(True)
+ self.valsLock.release()
+
+ def draw(self, width, height):
+ self.valsLock.acquire()
+
+ # draws the top label
+ 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"
+
+ # panel with details for the current selection
+ detailPanelHeight = self._config["features.config.selectionDetails.height"]
+ isScrollbarVisible = False
+ if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
+ # no detail panel
+ detailPanelHeight = 0
+ scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1)
+ cursorSelection = self.getSelection()
+ isScrollbarVisible = len(self._getConfigOptions()) > height - 1
+ else:
+ # Shrink detail panel if there isn't sufficient room for the whole
+ # thing. The extra line is for the bottom border.
+ detailPanelHeight = min(height - 1, detailPanelHeight + 1)
+ scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight)
+ cursorSelection = self.getSelection()
+ isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1
+
+ self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
+
+ 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 = self._config["features.config.state.colWidth.option"]
+ valueWidth = self._config["features.config.state.colWidth.value"]
+ descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
+
+ 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:
+ valueAttr = []
+ valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
+ 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 - 2, 4, 4, uiTools.Ending.HYPHEN, True)
+ descriptionContent = remainder.strip() + descriptionContent
+ else:
+ # this is the last line, end it with an ellipse
+ msg = uiTools.cropStr(lineContent, width - 2, 4, 4)
+
+ self.addstr(3 + i, 2, msg, selectionFormat)
+
diff --git a/src/cli/connections/__init__.py b/src/cli/connections/__init__.py
new file mode 100644
index 0000000..5babdde
--- /dev/null
+++ b/src/cli/connections/__init__.py
@@ -0,0 +1,6 @@
+"""
+Connection panel related resources.
+"""
+
+__all__ = ["circEntry", "connEntry", "connPanel", "entries"]
+
diff --git a/src/cli/connections/circEntry.py b/src/cli/connections/circEntry.py
new file mode 100644
index 0000000..b15b26a
--- /dev/null
+++ b/src/cli/connections/circEntry.py
@@ -0,0 +1,216 @@
+"""
+Connection panel entries for client circuits. This includes a header entry
+followed by an entry for each hop in the circuit. For instance:
+
+89.188.20.246:42667 --> 217.172.182.26 (de) General / Built 8.6m (CIRCUIT)
+| 85.8.28.4 (se) 98FBC3B2B93897A78CDD797EF549E6B62C9A8523 1 / Guard
+| 91.121.204.76 (fr) 546387D93F8D40CFF8842BB9D3A8EC477CEDA984 2 / Middle
++- 217.172.182.26 (de) 5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86 3 / Exit
+"""
+
+import curses
+
+from cli.connections import entries, connEntry
+from util import torTools, uiTools
+
+# cached fingerprint -> (IP Address, ORPort) results
+RELAY_INFO = {}
+
+def getRelayInfo(fingerprint):
+ """
+ Provides the (IP Address, ORPort) tuple for the given relay. If the lookup
+ fails then this returns ("192.168.0.1", "0").
+
+ Arguments:
+ fingerprint - relay to look up
+ """
+
+ if not fingerprint in RELAY_INFO:
+ conn = torTools.getConn()
+ failureResult = ("192.168.0.1", "0")
+
+ nsEntry = conn.getConsensusEntry(fingerprint)
+ if not nsEntry: return failureResult
+
+ nsLineComp = nsEntry.split("\n")[0].split(" ")
+ if len(nsLineComp) < 8: return failureResult
+
+ RELAY_INFO[fingerprint] = (nsLineComp[6], nsLineComp[7])
+
+ return RELAY_INFO[fingerprint]
+
+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]]
+
+ if status == "BUILT" and not self.lines[0].isBuilt:
+ exitIp, exitORPort = getRelayInfo(path[-1])
+ self.lines[0].setExit(exitIp, exitORPort, path[-1])
+
+ for i in range(len(path)):
+ relayFingerprint = path[i]
+ relayIp, relayOrPort = getRelayInfo(relayFingerprint)
+
+ 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):
+ """
+ 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()])
+ return [uiTools.DrawEntry("Building Circuit...", detailFormat)]
+ else: return connEntry.ConnectionLine.getDetails(self, width)
+
+class CircLine(connEntry.ConnectionLine):
+ """
+ An individual hop in a circuit. This overwrites the displayed listing, but
+ 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 getListingEntry(self, width, currentTime, listingType):
+ """
+ Provides the DrawEntry 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:
+ # bracketing (3 characters)
+ # placementLabel (14 characters)
+ # gap between etc and placement label (5 characters)
+
+ if self.isLast: bracket = (curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
+ else: bracket = (curses.ACS_VLINE, ord(' '), ord(' '))
+ baselineSpace = len(bracket) + 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)
+ etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
+ elif listingType == entries.ListingType.HOSTNAME:
+ # min space for the hostname is 40 characters
+ etc = self.getEtcContent(width - baselineSpace - 40, listingType)
+ dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
+ dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr())
+ elif listingType == entries.ListingType.FINGERPRINT:
+ # dst width is derived as:
+ # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
+ dst = "%-55s" % self.foreign.getFingerprint()
+ etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
+ else:
+ # min space for the nickname is 56 characters
+ etc = self.getEtcContent(width - baselineSpace - 56, listingType)
+ dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
+ dst = dstLayout % self.foreign.getNickname()
+
+ drawEntry = uiTools.DrawEntry("%-14s" % self.placementLabel, lineFormat)
+ drawEntry = uiTools.DrawEntry(" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat, drawEntry)
+ drawEntry = uiTools.DrawEntry(dst + etc, lineFormat, drawEntry)
+ drawEntry = uiTools.DrawEntry(bracket, curses.A_NORMAL, drawEntry, lockFormat = True)
+ return drawEntry
+
diff --git a/src/cli/connections/connEntry.py b/src/cli/connections/connEntry.py
new file mode 100644
index 0000000..ac45656
--- /dev/null
+++ b/src/cli/connections/connEntry.py
@@ -0,0 +1,850 @@
+"""
+Connection panel entries related to actual connections to or from the system
+(ie, results seen by netstat, lsof, etc).
+"""
+
+import time
+import curses
+
+from util import connections, enum, torTools, uiTools
+from cli.connections import entries
+
+# Connection Categories:
+# Inbound Relay connection, coming to us.
+# Outbound Relay connection, leaving us.
+# Exit Outbound relay connection leaving the Tor network.
+# Hidden Connections to a hidden service we're providing.
+# Socks Socks connections for applications using Tor.
+# Circuit Circuits our tor client has created.
+# Directory Fetching tor consensus information.
+# Control Tor controller (arm, vidalia, etc).
+
+Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
+CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue",
+ Category.EXIT: "red", Category.HIDDEN: "magenta",
+ Category.SOCKS: "yellow", Category.CIRCUIT: "cyan",
+ Category.DIRECTORY: "magenta", Category.CONTROL: "red"}
+
+# static data for listing format
+# <src> --> <dst> <etc><padding>
+LABEL_FORMAT = "%s --> %s %s%s"
+LABEL_MIN_PADDING = 2 # min space between listing label and following data
+
+# sort value for scrubbed ip addresses
+SCRUBBED_IP_VAL = 255 ** 4
+
+CONFIG = {"features.connection.markInitialConnections": True,
+ "features.connection.showExitPort": True,
+ "features.connection.showColumn.fingerprint": True,
+ "features.connection.showColumn.nickname": True,
+ "features.connection.showColumn.destination": True,
+ "features.connection.showColumn.expandedIp": True}
+
+def loadConfig(config):
+ config.update(CONFIG)
+
+class Endpoint:
+ """
+ Collection of attributes associated with a connection endpoint. This is a
+ 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 ORPort when searching for matching
+ # fingerprints (otherwise the ORPort is assumed to be unknown)
+ self.isORPort = False
+
+ # 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)
+ #except:
+ # # either a ValueError or IOError depending on the source of the lookup failure
+ # myHostname = None
+ #
+ #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()
+ orPort = self.port if self.isORPort else None
+ myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort)
+
+ 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"
+
+class ConnectionEntry(entries.ConnectionPanelEntry):
+ """
+ Represents a connection being made to or from this system. These only
+ 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
+ return connLine.sortIpAddr
+ elif attr == entries.SortAttr.PORT:
+ return connLine.sortPort
+ elif attr == entries.SortAttr.HOSTNAME:
+ if connLine.isPrivate(): return ""
+ return connLine.foreign.getHostname("")
+ elif attr == entries.SortAttr.FINGERPRINT:
+ return connLine.foreign.getFingerprint()
+ elif attr == entries.SortAttr.NICKNAME:
+ myNickname = connLine.foreign.getNickname()
+ if myNickname == "UNKNOWN": return "z" * 20 # orders at the end
+ else: return myNickname.lower()
+ elif attr == entries.SortAttr.CATEGORY:
+ return Category.indexOf(connLine.getType())
+ elif attr == entries.SortAttr.UPTIME:
+ return connLine.startTime
+ elif attr == entries.SortAttr.COUNTRY:
+ if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return ""
+ else: return connLine.foreign.getLocale("")
+ else:
+ return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType)
+
+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")
+
+ # 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")
+ myDirPort = conn.getOption("DirPort")
+ mySocksPort = conn.getOption("SocksPort", "9050")
+ myCtlPort = conn.getOption("ControlPort")
+ myHiddenServicePorts = conn.getHiddenServicePorts()
+
+ # the ORListenAddress can overwrite the ORPort
+ listenAddr = conn.getOption("ORListenAddress")
+ if listenAddr and ":" in listenAddr:
+ myOrPort = listenAddr[listenAddr.find(":") + 1:]
+
+ if lPort in (myOrPort, myDirPort):
+ self.baseType = Category.INBOUND
+ self.local.isORPort = True
+ elif lPort == mySocksPort:
+ self.baseType = Category.SOCKS
+ elif fPort in myHiddenServicePorts:
+ self.baseType = Category.HIDDEN
+ elif lPort == myCtlPort:
+ self.baseType = Category.CONTROL
+ else:
+ self.baseType = Category.OUTBOUND
+ self.foreign.isORPort = True
+
+ 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 DrawEntry 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 = ""
+
+ timeEntry = myListing.getNext()
+ timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 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:
+ # content - "<src> --> <dst> <etc> "
+ # time - "<uptime>"
+ # preType - " ("
+ # category - "<type>"
+ # postType - ") "
+
+ lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType])
+ timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5
+
+ drawEntry = uiTools.DrawEntry(")" + " " * (9 - len(entryType)), lineFormat)
+ drawEntry = uiTools.DrawEntry(entryType.upper(), lineFormat | curses.A_BOLD, drawEntry)
+ drawEntry = uiTools.DrawEntry(" (", lineFormat, drawEntry)
+ drawEntry = uiTools.DrawEntry(" " * timeWidth, lineFormat, drawEntry)
+ drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), listingType), lineFormat, drawEntry)
+ 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 [uiTools.DrawEntry(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.
+ """
+
+ # 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") == "1":
+ allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
+ return allMatches == []
+ 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:
+ if self.baseType == Category.OUTBOUND:
+ # Currently the only non-static categories are OUTBOUND vs...
+ # - 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[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[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
+ if listingType == entries.ListingType.IP_ADDRESS:
+ 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 + 10 and CONFIG["features.connection.showColumn.nickname"]:
+ # show nickname (column width: remainder)
+ nicknameSpace = width - usedSpace
+ nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+ etc += ("%%-%is " % nicknameSpace) % nicknameLabel
+ usedSpace += nicknameSpace + 2
+ elif listingType == entries.ListingType.HOSTNAME:
+ if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
+ # 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
+ nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+ etc += ("%%-%is " % nicknameSpace) % nicknameLabel
+ usedSpace += (nicknameSpace + 2)
+ elif listingType == entries.ListingType.FINGERPRINT:
+ 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
+ else:
+ 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 + 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.
+ #
+ # 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:
+ # 15 characters for source, and a min of 40 reserved for the destination
+ # TODO: when actually functional the src and dst need to be swapped for
+ # SOCKS and CONTROL connections
+ 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():
+ dst = ("%%-%is" % hostnameSpace) % "<scrubbed>"
+ 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)
+ elif listingType == entries.ListingType.FINGERPRINT:
+ src = "localhost"
+ 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:
+ # base data requires 50 min characters
+ src = self.local.getNickname()
+ 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:]
+
+ # The network status exit policy doesn't exist for older tor versions.
+ # If unavailable we'll need the full exit policy which is on the
+ # descriptor (if that's available).
+
+ exitPolicy = "unknown"
+ if len(nsLines) >= 4 and nsLines[3].startswith("p "):
+ exitPolicy = nsLines[3][2:].replace(",", ", ")
+ elif descEntry:
+ # the descriptor has an individual line for each entry in the exit policy
+ exitPolicyEntries = []
+
+ for line in descEntry.split("\n"):
+ if line.startswith("accept") or line.startswith("reject"):
+ exitPolicyEntries.append(line.strip())
+
+ exitPolicy = ", ".join(exitPolicyEntries)
+
+ dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
+ lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
+ lines[3] = "published: %s %s" % (pubDate, pubTime)
+ lines[4] = "flags: %s" % flags.replace(" ", ", ")
+ lines[5] = "exit policy: %s" % exitPolicy
+
+ 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/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
new file mode 100644
index 0000000..569f57c
--- /dev/null
+++ b/src/cli/connections/connPanel.py
@@ -0,0 +1,398 @@
+"""
+Listing of the currently established connections tor has made.
+"""
+
+import time
+import curses
+import threading
+
+from cli.connections import entries, connEntry, circEntry
+from util import connections, enum, panel, torTools, uiTools
+
+DEFAULT_CONFIG = {"features.connection.resolveApps": True,
+ "features.connection.listingType": 0,
+ "features.connection.refreshRate": 5}
+
+# height of the detail panel content, not counting top and bottom border
+DETAILS_HEIGHT = 7
+
+# listing types
+Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME)
+
+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, config=None):
+ panel.Panel.__init__(self, stdscr, "conn", 0)
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+
+ self._sortOrdering = DEFAULT_SORT_ORDER
+ self._config = dict(DEFAULT_CONFIG)
+
+ if config:
+ config.update(self._config, {
+ "features.connection.listingType": (0, len(Listing.values()) - 1),
+ "features.connection.refreshRate": 1})
+
+ sortFields = entries.SortAttr.values()
+ customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
+
+ if customOrdering:
+ self._sortOrdering = [sortFields[i] for i in customOrdering]
+
+ self._listingType = Listing.values()[self._config["features.connection.listingType"]]
+ 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._isPaused = True # prevents updates if true
+ self._pauseTime = None # time when the panel was paused
+ self._halt = False # terminates thread if true
+ self._cond = threading.Condition() # used for pausing the thread
+ self.valsLock = threading.RLock()
+
+ # 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
+
+ self._update() # populates initial entries
+ self._resolveApps(False) # resolves initial applications
+
+ # 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
+ torTools.getConn().addStatusListener(self.torStateListener)
+
+ def torStateListener(self, conn, eventType):
+ """
+ Freezes the connection contents when Tor stops.
+
+ Arguments:
+ conn - tor controller
+ eventType - type of event detected
+ """
+
+ self._isTorRunning = eventType == torTools.State.INIT
+
+ if self._isPaused or not self._isTorRunning:
+ if not self._pauseTime: self._pauseTime = time.time()
+ else: self._pauseTime = None
+
+ self.redraw(True)
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents the panel from updating.
+ """
+
+ if not self._isPaused == isPause:
+ self._isPaused = isPause
+
+ if isPause or not self._isTorRunning:
+ if not self._pauseTime: self._pauseTime = time.time()
+ else: self._pauseTime = None
+
+ # redraws so the display reflects any changes between the last update
+ # and being paused
+ self.redraw(True)
+
+ 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: self._sortOrdering = ordering
+ self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
+
+ self._entryLines = []
+ for entry in self._entries:
+ self._entryLines += entry.getLines()
+ self.valsLock.release()
+
+ def setListingType(self, listingType):
+ """
+ Sets the priority information presented by the panel.
+
+ Arguments:
+ listingType - Listing instance for the primary information to be shown
+ """
+
+ self.valsLock.acquire()
+ self._listingType = listingType
+
+ # if we're sorting by the listing then we need to resort
+ if entries.SortAttr.LISTING in self._sortOrdering:
+ self.setSortOrder()
+
+ self.valsLock.release()
+
+ def handleKey(self, key):
+ self.valsLock.acquire()
+
+ if uiTools.isScrollKey(key):
+ pageHeight = self.getPreferredSize()[0] - 1
+ if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
+ isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
+ if isChanged: self.redraw(True)
+ elif uiTools.isSelectionKey(key):
+ self._showDetails = not self._showDetails
+ self.redraw(True)
+
+ self.valsLock.release()
+
+ def run(self):
+ """
+ Keeps connections listing updated, checking for new entries at a set rate.
+ """
+
+ lastDraw = time.time() - 1
+ while not self._halt:
+ currentTime = time.time()
+
+ if self._isPaused or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]:
+ self._cond.acquire()
+ if not self._halt: self._cond.wait(0.2)
+ self._cond.release()
+ else:
+ # 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) / self._config["features.connection.refreshRate"]
+ lastDraw += self._config["features.connection.refreshRate"] * drawTicks
+
+ def draw(self, width, height):
+ self.valsLock.acquire()
+
+ # 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._scroller.getCursorSelection(self._entryLines)
+
+ # draws the detail panel if currently displaying it
+ if self._showDetails:
+ # 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)):
+ drawEntries[i].render(self, 1 + i, 2)
+
+ # title label with connection counts
+ title = "Connection Details:" if self._showDetails else self._title
+ self.addstr(0, 0, title, curses.A_STANDOUT)
+
+ scrollOffset = 1
+ if isScrollbarVisible:
+ scrollOffset = 3
+ self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset)
+
+ currentTime = self._pauseTime if self._pauseTime else 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
+
+ drawEntry = entryLine.getListingEntry(width - scrollOffset, currentTime, self._listingType)
+ drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
+ drawEntry.render(self, drawLine, scrollOffset, extraFormat)
+ 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.
+ """
+
+ connResolver = connections.getResolver("tor")
+ currentResolutionCount = connResolver.getResolutionCount()
+ self.appResolveSinceUpdate = False
+
+ if self._lastResourceFetch != currentResolutionCount:
+ 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)
+ del newCircuits[oldEntry.circuitID]
+ elif isinstance(oldEntry, connEntry.ConnectionEntry):
+ 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)
+ if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT:
+ newEntries.append(newConnEntry)
+
+ 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 = connEntry.Category.values()
+ typeCounts = dict((type, 0) for type in categoryTypes)
+ for entry in newEntries:
+ if isinstance(entry, connEntry.ConnectionEntry):
+ 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 self._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.
+ #
+ # The application resolver might have given up querying (for instance, if
+ # 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/src/cli/connections/entries.py b/src/cli/connections/entries.py
new file mode 100644
index 0000000..6b24412
--- /dev/null
+++ b/src/cli/connections/entries.py
@@ -0,0 +1,164 @@
+"""
+Interface for entries in the connection panel. These consist of two parts: the
+entry itself (ie, Tor connection, client circuit, etc) and the lines it
+consists of in the listing.
+"""
+
+from util import enum
+
+# attributes we can list entries by
+ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT",
+ "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
+
+SORT_COLORS = {SortAttr.CATEGORY: "red", SortAttr.UPTIME: "yellow",
+ SortAttr.LISTING: "green", SortAttr.IP_ADDRESS: "blue",
+ SortAttr.PORT: "blue", SortAttr.HOSTNAME: "magenta",
+ SortAttr.FINGERPRINT: "cyan", SortAttr.NICKNAME: "cyan",
+ SortAttr.COUNTRY: "blue"}
+
+# maximum number of ports a system can have
+PORT_COUNT = 65536
+
+class ConnectionPanelEntry:
+ """
+ Common parent for connection panel entries. This consists of a list of lines
+ 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
+ sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT
+ sortValue += self.getSortValue(SortAttr.PORT, listingType)
+ return sortValue
+ elif listingType == ListingType.HOSTNAME:
+ return self.getSortValue(SortAttr.HOSTNAME, listingType)
+ elif listingType == ListingType.FINGERPRINT:
+ 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 getListingEntry(self, width, currentTime, listingType):
+ """
+ Provides a DrawEntry instance 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
+ the current time (this may be ignored due to caching)
+ 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 DrawEntry instances 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/src/cli/controller.py b/src/cli/controller.py
new file mode 100644
index 0000000..2afbf6a
--- /dev/null
+++ b/src/cli/controller.py
@@ -0,0 +1,1584 @@
+#!/usr/bin/env python
+# controller.py -- arm interface (curses monitor for relay status)
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+"""
+Curses (terminal) interface for the arm relay status monitor.
+"""
+
+import os
+import re
+import math
+import time
+import curses
+import curses.textpad
+import socket
+from TorCtl import TorCtl
+
+import headerPanel
+import graphing.graphPanel
+import logPanel
+import configPanel
+import torrcPanel
+import descriptorPopup
+
+import cli.connections.connPanel
+import cli.connections.connEntry
+import cli.connections.entries
+from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools
+import graphing.bandwidthStats
+import graphing.connStats
+import graphing.resourceStats
+
+CONFIRM_QUIT = True
+REFRESH_RATE = 5 # seconds between redrawing screen
+MAX_REGEX_FILTERS = 5 # maximum number of previous regex filters that'll be remembered
+
+# enums for message in control label
+CTL_HELP, CTL_PAUSED = range(2)
+
+# panel order per page
+PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
+PAGES = [
+ ["graph", "log"],
+ ["conn"],
+ ["config"],
+ ["torrc"]]
+
+PAUSEABLE = ["header", "graph", "log", "conn"]
+
+CONFIG = {"log.torrc.readFailed": log.WARN,
+ "features.graph.type": 1,
+ "features.config.prepopulateEditValues": True,
+ "queries.refreshRate.rate": 5,
+ "log.torEventTypeUnrecognized": log.NOTICE,
+ "features.graph.bw.prepopulate": True,
+ "log.startTime": log.INFO,
+ "log.refreshRate": log.DEBUG,
+ "log.highCpuUsage": log.WARN,
+ "log.configEntryUndefined": log.NOTICE,
+ "log.torrc.validation.torStateDiffers": log.WARN,
+ "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE}
+
+class ControlPanel(panel.Panel):
+ """ Draws single line label for interface controls. """
+
+ def __init__(self, stdscr, isBlindMode):
+ panel.Panel.__init__(self, stdscr, "control", 0, 1)
+ self.msgText = CTL_HELP # message text to be displyed
+ self.msgAttr = curses.A_NORMAL # formatting attributes
+ self.page = 1 # page number currently being displayed
+ self.resolvingCounter = -1 # count of resolver when starting (-1 if we aren't working on a batch)
+ self.isBlindMode = isBlindMode
+
+ def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
+ """
+ Sets the message and display attributes. If msgType matches CTL_HELP or
+ CTL_PAUSED then uses the default message for those statuses.
+ """
+
+ self.msgText = msgText
+ self.msgAttr = msgAttr
+
+ def draw(self, width, height):
+ msgText = self.msgText
+ msgAttr = self.msgAttr
+ barTab = 2 # space between msgText and progress bar
+ barWidthMax = 40 # max width to progress bar
+ barWidth = -1 # space between "[ ]" in progress bar (not visible if -1)
+ barProgress = 0 # cells to fill
+
+ if msgText == CTL_HELP:
+ msgAttr = curses.A_NORMAL
+
+ if self.resolvingCounter != -1:
+ if hostnames.isPaused() or not hostnames.isResolving():
+ # done resolving dns batch
+ self.resolvingCounter = -1
+ curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate
+ else:
+ batchSize = hostnames.getRequestCount() - self.resolvingCounter
+ entryCount = batchSize - hostnames.getPendingCount()
+ if batchSize > 0: progress = 100 * entryCount / batchSize
+ else: progress = 0
+
+ additive = "or l " if self.page == 2 else ""
+ batchSizeDigits = int(math.log10(batchSize)) + 1
+ entryCountLabel = ("%%%ii" % batchSizeDigits) % entryCount
+ #msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
+ msgText = "Resolving hostnames (press esc %sto cancel) - %s / %i, %2i%%" % (additive, entryCountLabel, batchSize, progress)
+
+ barWidth = min(barWidthMax, width - len(msgText) - 3 - barTab)
+ barProgress = barWidth * entryCount / batchSize
+
+ if self.resolvingCounter == -1:
+ currentPage = self.page
+ pageCount = len(PAGES)
+
+ if self.isBlindMode:
+ if currentPage >= 2: currentPage -= 1
+ pageCount -= 1
+
+ msgText = "page %i / %i - q: quit, p: pause, h: page help" % (currentPage, pageCount)
+ elif msgText == CTL_PAUSED:
+ msgText = "Paused"
+ msgAttr = curses.A_STANDOUT
+
+ self.addstr(0, 0, msgText, msgAttr)
+ if barWidth > -1:
+ xLoc = len(msgText) + barTab
+ self.addstr(0, xLoc, "[", curses.A_BOLD)
+ self.addstr(0, xLoc + 1, " " * barProgress, curses.A_STANDOUT | uiTools.getColor("red"))
+ self.addstr(0, xLoc + barWidth + 1, "]", curses.A_BOLD)
+
+class Popup(panel.Panel):
+ """
+ Temporarily providing old panel methods until permanent workaround for popup
+ can be derrived (this passive drawing method is horrible - I'll need to
+ provide a version using the more active repaint design later in the
+ revision).
+ """
+
+ def __init__(self, stdscr, height):
+ panel.Panel.__init__(self, stdscr, "popup", 0, height)
+
+ # The following methods are to emulate old panel functionality (this was the
+ # only implementations to use these methods and will require a complete
+ # rewrite when refactoring gets here)
+ def clear(self):
+ if self.win:
+ self.isDisplaced = self.top > self.win.getparyx()[0]
+ if not self.isDisplaced: self.win.erase()
+
+ def refresh(self):
+ if self.win and not self.isDisplaced: self.win.refresh()
+
+ def recreate(self, stdscr, newWidth=-1, newTop=None):
+ self.setParent(stdscr)
+ self.setWidth(newWidth)
+ if newTop != None: self.setTop(newTop)
+
+ newHeight, newWidth = self.getPreferredSize()
+ if newHeight > 0:
+ self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
+ elif self.win == None:
+ # don't want to leave the window as none (in very edge cases could cause
+ # problems) - rather, create a displaced instance
+ self.win = self.parent.subwin(1, newWidth, 0, 0)
+
+ self.maxY, self.maxX = self.win.getmaxyx()
+
+def addstr_wrap(panel, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
+ """
+ Writes text with word wrapping, returning the ending y/x coordinate.
+ y: starting write line
+ x: column offset from startX
+ text / formatting: content to be written
+ startX / endX: column bounds in which text may be written
+ """
+
+ # moved out of panel (trying not to polute new code!)
+ # TODO: unpleaseantly complex usage - replace with something else when
+ # rewriting confPanel and descriptorPopup (the only places this is used)
+ if not text: return (y, x) # nothing to write
+ if endX == -1: endX = panel.maxX # defaults to writing to end of panel
+ if maxY == -1: maxY = panel.maxY + 1 # defaults to writing to bottom of panel
+ lineWidth = endX - startX # room for text
+ while True:
+ if len(text) > lineWidth - x - 1:
+ chunkSize = text.rfind(" ", 0, lineWidth - x)
+ writeText = text[:chunkSize]
+ text = text[chunkSize:].strip()
+
+ panel.addstr(y, x + startX, writeText, formatting)
+ y, x = y + 1, 0
+ if y >= maxY: return (y, x)
+ else:
+ panel.addstr(y, x + startX, text, formatting)
+ return (y, x + len(text))
+
+class sighupListener(TorCtl.PostEventListener):
+ """
+ Listens for reload signal (hup), which is produced by:
+ pkill -sighup tor
+ causing the torrc and internal state to be reset.
+ """
+
+ def __init__(self):
+ TorCtl.PostEventListener.__init__(self)
+ self.isReset = False
+
+ def msg_event(self, event):
+ self.isReset |= event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)")
+
+def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
+ """
+ Resets the isPaused state of panels. If overwrite is True then this pauses
+ reguardless of the monitor is paused or not.
+ """
+
+ for key in PAUSEABLE: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S))
+
+def showMenu(stdscr, popup, title, options, initialSelection):
+ """
+ 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. If initialSelection is -1 then the first
+ option is used and the carrot indicating past selection is ommitted.
+ """
+
+ selection = initialSelection if initialSelection != -1 else 0
+
+ if popup.win:
+ if not panel.CURSES_LOCK.acquire(False): return -1
+ try:
+ # TODO: should pause interface (to avoid event accumilation)
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+
+ # uses smaller dimentions more fitting for small content
+ popup.height = len(options) + 2
+
+ newWidth = max([len(label) for label in options]) + 9
+ popup.recreate(stdscr, newWidth)
+
+ key = 0
+ while not uiTools.isSelectionKey(key):
+ popup.clear()
+ 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 == initialSelection else " "
+ popup.addstr(i + 1, 2, tab)
+ popup.addstr(i + 1, 4, " %s " % label, format)
+
+ popup.refresh()
+ key = stdscr.getch()
+ if key == curses.KEY_UP: selection = max(0, selection - 1)
+ elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
+ elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
+
+ # reverts popup dimensions and conn panel label
+ popup.height = 9
+ popup.recreate(stdscr, 80)
+
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ panel.CURSES_LOCK.release()
+
+ return selection
+
+def showSortDialog(stdscr, panels, isPaused, page, titleLabel, 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:
+ stdscr, panels, isPaused, page - boiler plate arguments of the controller
+ (should be refactored away when rewriting)
+
+ titleLabel - title displayed for the popup window
+ options - ordered listing of option labels
+ oldSelection - current ordering
+ optionColors - mappings of options to their color
+
+ """
+
+ panel.CURSES_LOCK.acquire()
+ newSelections = [] # new ordering
+
+ try:
+ setPauseState(panels, isPaused, page, True)
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+
+ popup = panels["popup"]
+ cursorLoc = 0 # index of highlighted option
+
+ # label for the inital ordering
+ formattedPrevListing = []
+ for sortType in oldSelection:
+ colorStr = optionColors.get(sortType, "white")
+ formattedPrevListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
+ prevOrderingLabel = "<b>Current Order: %s</b>" % ", ".join(formattedPrevListing)
+
+ selectionOptions = list(options)
+ selectionOptions.append("Cancel")
+
+ while len(newSelections) < len(oldSelection):
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+ popup.addfstr(1, 2, prevOrderingLabel)
+
+ # provides new ordering
+ formattedNewListing = []
+ for sortType in newSelections:
+ colorStr = optionColors.get(sortType, "white")
+ formattedNewListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
+ newOrderingLabel = "<b>New Order: %s</b>" % ", ".join(formattedNewListing)
+ popup.addfstr(2, 2, newOrderingLabel)
+
+ # presents remaining options, each row having up to four options with
+ # spacing of nineteen cells
+ row, col = 4, 0
+ for i in range(len(selectionOptions)):
+ popup.addstr(row, col * 19 + 2, selectionOptions[i], curses.A_STANDOUT if cursorLoc == i else curses.A_NORMAL)
+ col += 1
+ if col == 4: row, col = row + 1, 0
+
+ popup.refresh()
+
+ key = stdscr.getch()
+ if key == curses.KEY_LEFT: cursorLoc = max(0, cursorLoc - 1)
+ elif key == curses.KEY_RIGHT: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 1)
+ elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4)
+ elif key == curses.KEY_DOWN: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 4)
+ elif uiTools.isSelectionKey(key):
+ # selected entry (the ord of '10' seems needed to pick up enter)
+ selection = selectionOptions[cursorLoc]
+ if selection == "Cancel": break
+ else:
+ newSelections.append(selection)
+ selectionOptions.remove(selection)
+ cursorLoc = min(cursorLoc, len(selectionOptions) - 1)
+ elif key == 27: break # esc - cancel
+
+ setPauseState(panels, isPaused, page)
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ panel.CURSES_LOCK.release()
+
+ if len(newSelections) == len(oldSelection):
+ return newSelections
+ else: return None
+
+def setEventListening(selectedEvents, isBlindMode):
+ # creates a local copy, note that a suspected python bug causes *very*
+ # puzzling results otherwise when trying to discard entries (silently
+ # returning out of this function!)
+ events = set(selectedEvents)
+ isLoggingUnknown = "UNKNOWN" in events
+
+ # removes special types only used in arm (UNKNOWN, TORCTL, ARM_DEBUG, etc)
+ toDiscard = []
+ for eventType in events:
+ if eventType not in logPanel.TOR_EVENT_TYPES.values(): toDiscard += [eventType]
+
+ for eventType in list(toDiscard): events.discard(eventType)
+
+ # adds events unrecognized by arm if we're listening to the 'UNKNOWN' type
+ if isLoggingUnknown:
+ events.update(set(logPanel.getMissingEventTypes()))
+
+ setEvents = torTools.getConn().setControllerEvents(list(events))
+
+ # temporary hack for providing user selected events minus those that failed
+ # (wouldn't be a problem if I wasn't storing tor and non-tor events together...)
+ returnVal = list(selectedEvents.difference(torTools.FAILED_EVENTS))
+ returnVal.sort() # alphabetizes
+ return returnVal
+
+def connResetListener(conn, eventType):
+ """
+ Pauses connection resolution when tor's shut down, and resumes if started
+ again.
+ """
+
+ if connections.isResolverAlive("tor"):
+ resolver = connections.getResolver("tor")
+ resolver.setPaused(eventType == torTools.State.CLOSED)
+
+def selectiveRefresh(panels, page):
+ """
+ This forces a redraw of content on the currently active page (should be done
+ after changing pages, popups, or anything else that overwrites panels).
+ """
+
+ for panelKey in PAGES[page]:
+ panels[panelKey].redraw(True)
+
+def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
+ """
+ Starts arm interface reflecting information on provided control port.
+
+ stdscr - curses window
+ conn - active Tor control port connection
+ loggedEvents - types of events to be logged (plus an optional "UNKNOWN" for
+ otherwise unrecognized events)
+ """
+
+ # loads config for various interface components
+ config = conf.getConfig("arm")
+ config.update(CONFIG)
+ graphing.graphPanel.loadConfig(config)
+ cli.connections.connEntry.loadConfig(config)
+
+ # 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 isBlindMode:
+ torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections"
+
+ # pauses/unpauses connection resolution according to if tor's connected or not
+ torTools.getConn().addStatusListener(connResetListener)
+
+ # TODO: incrementally drop this requirement until everything's using the singleton
+ conn = torTools.getConn().getTorCtl()
+
+ curses.halfdelay(REFRESH_RATE * 10) # uses getch call as timer for REFRESH_RATE seconds
+ try: curses.use_default_colors() # allows things like semi-transparent backgrounds (call can fail with ERR)
+ except curses.error: pass
+
+ # attempts to make the cursor invisible (not supported in all terminals)
+ try: curses.curs_set(0)
+ except curses.error: pass
+
+ # attempts to determine tor's current pid (left as None if unresolveable, logging an error later)
+ torPid = torTools.getConn().getMyPid()
+
+ #try:
+ # confLocation = conn.get_info("config-file")["config-file"]
+ # if confLocation[0] != "/":
+ # # relative path - attempt to add process pwd
+ # try:
+ # results = sysTools.call("pwdx %s" % torPid)
+ # if len(results) == 1 and len(results[0].split()) == 2: confLocation = "%s/%s" % (results[0].split()[1], confLocation)
+ # except IOError: pass # pwdx call failed
+ #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+ # confLocation = ""
+
+ # loads the torrc and provides warnings in case of validation errors
+ loadedTorrc = torConfig.getTorrc()
+ loadedTorrc.getLock().acquire()
+
+ try:
+ loadedTorrc.load()
+ except IOError, exc:
+ msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
+ log.log(CONFIG["log.torrc.readFailed"], msg)
+
+ if loadedTorrc.isLoaded():
+ corrections = loadedTorrc.getCorrections()
+ duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], []
+
+ for lineNum, issue, msg in corrections:
+ if issue == torConfig.ValidationError.DUPLICATE:
+ duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1))
+ elif issue == torConfig.ValidationError.IS_DEFAULT:
+ defaultOptions.append("%s (line %i)" % (msg, lineNum + 1))
+ elif issue == torConfig.ValidationError.MISMATCH: mismatchLines.append(lineNum + 1)
+ elif issue == torConfig.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.log(CONFIG["log.torrc.validation.unnecessaryTorrcEntries"], msg)
+
+ if mismatchLines or missingOptions:
+ msg = "The torrc differ 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.log(CONFIG["log.torrc.validation.torStateDiffers"], msg)
+
+ loadedTorrc.getLock().release()
+
+ # minor refinements for connection resolver
+ if not isBlindMode:
+ if torPid:
+ # use the tor pid to help narrow connection results
+ torCmdName = sysTools.getProcessName(torPid, "tor")
+ resolver = connections.getResolver(torCmdName, torPid, "tor")
+ else:
+ resolver = connections.getResolver("tor")
+
+ # hack to display a better (arm specific) notice if all resolvers fail
+ connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
+
+ panels = {
+ "header": headerPanel.HeaderPanel(stdscr, startTime, config),
+ "popup": Popup(stdscr, 9),
+ "graph": graphing.graphPanel.GraphPanel(stdscr),
+ "log": logPanel.LogPanel(stdscr, loggedEvents, config)}
+
+ # TODO: later it would be good to set the right 'top' values during initialization,
+ # but for now this is just necessary for the log panel (and a hack in the log...)
+
+ # TODO: bug from not setting top is that the log panel might attempt to draw
+ # before being positioned - the following is a quick hack til rewritten
+ panels["log"].setPaused(True)
+
+ panels["conn"] = cli.connections.connPanel.ConnectionPanel(stdscr, config)
+
+ panels["control"] = ControlPanel(stdscr, isBlindMode)
+ panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.State.TOR, config)
+ panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.Config.TORRC, config)
+
+ # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
+ if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
+
+ # statistical monitors for graph
+ panels["graph"].addStats("bandwidth", graphing.bandwidthStats.BandwidthStats(config))
+ panels["graph"].addStats("system resources", graphing.resourceStats.ResourceStats())
+ if not isBlindMode: panels["graph"].addStats("connections", graphing.connStats.ConnStats())
+
+ # sets graph based on config parameter
+ graphType = CONFIG["features.graph.type"]
+ if graphType == 0: panels["graph"].setStats(None)
+ elif graphType == 1: panels["graph"].setStats("bandwidth")
+ elif graphType == 2 and not isBlindMode: panels["graph"].setStats("connections")
+ elif graphType == 3: panels["graph"].setStats("system resources")
+
+ # listeners that update bandwidth and log panels with Tor status
+ sighupTracker = sighupListener()
+ #conn.add_event_listener(panels["log"])
+ conn.add_event_listener(panels["graph"].stats["bandwidth"])
+ conn.add_event_listener(panels["graph"].stats["system resources"])
+ if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
+ conn.add_event_listener(sighupTracker)
+
+ # prepopulates bandwidth values from state file
+ if CONFIG["features.graph.bw.prepopulate"]:
+ isSuccessful = panels["graph"].stats["bandwidth"].prepopulateFromState()
+ if isSuccessful: panels["graph"].updateInterval = 4
+
+ # tells Tor to listen to the events we're interested
+ loggedEvents = setEventListening(loggedEvents, isBlindMode)
+ #panels["log"].loggedEvents = loggedEvents # strips any that couldn't be set
+ panels["log"].setLoggedEvents(loggedEvents) # strips any that couldn't be set
+
+ # directs logged TorCtl events to log panel
+ #TorUtil.loglevel = "DEBUG"
+ #TorUtil.logfile = panels["log"]
+ #torTools.getConn().addTorCtlListener(panels["log"].tor_ctl_event)
+
+ # provides a notice about any event types tor supports but arm doesn't
+ missingEventTypes = logPanel.getMissingEventTypes()
+ if missingEventTypes:
+ pluralLabel = "s" if len(missingEventTypes) > 1 else ""
+ log.log(CONFIG["log.torEventTypeUnrecognized"], "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)))
+
+ # tells revised panels to run as daemons
+ panels["header"].start()
+ panels["log"].start()
+ panels["conn"].start()
+
+ # warns if tor isn't updating descriptors
+ #try:
+ # if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
+ # warning = """Descriptors won't be updated (causing some connection information to be stale) unless:
+ #a. 'FetchUselessDescriptors 1' is set in your torrc
+ #b. the directory service is provided ('DirPort' defined)
+ #c. or tor is used as a client"""
+ # log.log(log.WARN, warning)
+ #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+
+ isUnresponsive = False # true if it's been over ten seconds since the last BW event (probably due to Tor closing)
+ isPaused = False # if true updates are frozen
+ overrideKey = None # immediately runs with this input rather than waiting for the user if set
+ page = 0
+ regexFilters = [] # previously used log regex filters
+ panels["popup"].redraw(True) # hack to make sure popup has a window instance (not entirely sure why...)
+
+ # provides notice about any unused config keys
+ for key in config.getUnusedKeys():
+ log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
+
+ lastPerformanceLog = 0 # ensures we don't do performance logging too frequently
+ redrawStartTime = time.time()
+
+ # TODO: popups need to force the panels it covers to redraw (or better, have
+ # a global refresh function for after changing pages, popups, etc)
+
+ initTime = time.time() - startTime
+ log.log(CONFIG["log.startTime"], "arm started (initialization took %0.3f seconds)" % initTime)
+
+ # attributes to give a WARN level event if arm's resource usage is too high
+ isResourceWarningGiven = False
+ lastResourceCheck = startTime
+
+ lastSize = None
+
+ # sets initial visiblity for the pages
+ for i in range(len(PAGES)):
+ isVisible = i == page
+ for entry in PAGES[i]: panels[entry].setVisible(isVisible)
+
+ # TODO: come up with a nice, clean method for other threads to immediately
+ # terminate the draw loop and provide a stacktrace
+ while True:
+ # tried only refreshing when the screen was resized but it caused a
+ # noticeable lag when resizing and didn't have an appreciable effect
+ # on system usage
+
+ panel.CURSES_LOCK.acquire()
+ try:
+ redrawStartTime = time.time()
+
+ # if sighup received then reload related information
+ if sighupTracker.isReset:
+ #panels["header"]._updateParams(True)
+
+ # other panels that use torrc data
+ #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
+ #panels["graph"].stats["bandwidth"].resetOptions()
+
+ # if bandwidth graph is being shown then height might have changed
+ if panels["graph"].currentDisplay == "bandwidth":
+ panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getContentHeight())
+
+ # TODO: should redraw the torrcPanel
+ #panels["torrc"].loadConfig()
+
+ # reload the torrc if it's previously been loaded
+ if loadedTorrc.isLoaded():
+ try:
+ loadedTorrc.load()
+ if page == 3: panels["torrc"].redraw(True)
+ except IOError, exc:
+ msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
+ log.log(CONFIG["log.torrc.readFailed"], msg)
+
+ sighupTracker.isReset = False
+
+ # gives panels a chance to take advantage of the maximum bounds
+ # originally this checked in the bounds changed but 'recreate' is a no-op
+ # if panel properties are unchanged and checking every redraw is more
+ # resilient in case of funky changes (such as resizing during popups)
+
+ # hack to make sure header picks layout before using the dimensions below
+ #panels["header"].getPreferredSize()
+
+ startY = 0
+ for panelKey in PAGE_S[:2]:
+ #panels[panelKey].recreate(stdscr, -1, startY)
+ panels[panelKey].setParent(stdscr)
+ panels[panelKey].setWidth(-1)
+ panels[panelKey].setTop(startY)
+ startY += panels[panelKey].getHeight()
+
+ panels["popup"].recreate(stdscr, 80, startY)
+
+ for panelSet in PAGES:
+ tmpStartY = startY
+
+ for panelKey in panelSet:
+ #panels[panelKey].recreate(stdscr, -1, tmpStartY)
+ panels[panelKey].setParent(stdscr)
+ panels[panelKey].setWidth(-1)
+ panels[panelKey].setTop(tmpStartY)
+ tmpStartY += panels[panelKey].getHeight()
+
+ # provides a notice if there's been ten seconds since the last BW event
+ lastHeartbeat = torTools.getConn().getHeartbeat()
+ if torTools.getConn().isAlive() and "BW" in torTools.getConn().getControllerEvents() and lastHeartbeat != 0:
+ if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
+ isUnresponsive = True
+ log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
+ elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
+ # really shouldn't happen (meant Tor froze for a bit)
+ isUnresponsive = False
+ log.log(log.NOTICE, "Relay resumed")
+
+ # TODO: part two of hack to prevent premature drawing by log panel
+ if page == 0 and not isPaused: panels["log"].setPaused(False)
+
+ # I haven't the foggiest why, but doesn't work if redrawn out of order...
+ for panelKey in (PAGE_S + PAGES[page]):
+ # redrawing popup can result in display flicker when it should be hidden
+ if panelKey != "popup":
+ newSize = stdscr.getmaxyx()
+ isResize = lastSize != newSize
+ lastSize = newSize
+
+ if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"):
+ # revised panel (manages its own content refreshing)
+ panels[panelKey].redraw(isResize)
+ else:
+ panels[panelKey].redraw(True)
+
+ stdscr.refresh()
+
+ currentTime = time.time()
+ if currentTime - lastPerformanceLog >= CONFIG["queries.refreshRate.rate"]:
+ cpuTotal = sum(os.times()[:3])
+ pythonCpuAvg = cpuTotal / (currentTime - startTime)
+ sysCallCpuAvg = sysTools.getSysCpuUsage()
+ totalCpuAvg = pythonCpuAvg + sysCallCpuAvg
+
+ if sysCallCpuAvg > 0.00001:
+ log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%% (python), %0.3f%% (system calls), %0.3f%% (total)" % (currentTime - redrawStartTime, 100 * pythonCpuAvg, 100 * sysCallCpuAvg, 100 * totalCpuAvg))
+ else:
+ # with the proc enhancements the sysCallCpuAvg is usually zero
+ log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%%" % (currentTime - redrawStartTime, 100 * totalCpuAvg))
+
+ lastPerformanceLog = currentTime
+
+ # once per minute check if the sustained cpu usage is above 5%, if so
+ # then give a warning (and if able, some advice for lowering it)
+ # TODO: disabling this for now (scrolling causes cpu spikes for quick
+ # redraws, ie this is usually triggered by user input)
+ if False and not isResourceWarningGiven and currentTime > (lastResourceCheck + 60):
+ if totalCpuAvg >= 0.05:
+ msg = "Arm's cpu usage is high (averaging %0.3f%%)." % (100 * totalCpuAvg)
+
+ if not isBlindMode:
+ msg += " You could lower it by dropping the connection data (running as \"arm -b\")."
+
+ log.log(CONFIG["log.highCpuUsage"], msg)
+ isResourceWarningGiven = True
+
+ lastResourceCheck = currentTime
+ finally:
+ panel.CURSES_LOCK.release()
+
+ # wait for user keyboard input until timeout (unless an override was set)
+ if overrideKey:
+ key = overrideKey
+ overrideKey = None
+ else:
+ key = stdscr.getch()
+
+ if key == ord('q') or key == ord('Q'):
+ quitConfirmed = not CONFIRM_QUIT
+
+ # provides prompt to confirm that arm should exit
+ if CONFIRM_QUIT:
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # provides prompt
+ panels["control"].setMsg("Are you sure (q again to confirm)?", curses.A_BOLD)
+ panels["control"].redraw(True)
+
+ curses.cbreak()
+ confirmationKey = stdscr.getch()
+ quitConfirmed = confirmationKey in (ord('q'), ord('Q'))
+ curses.halfdelay(REFRESH_RATE * 10)
+
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ setPauseState(panels, isPaused, page)
+ finally:
+ panel.CURSES_LOCK.release()
+
+ if quitConfirmed:
+ # quits arm
+ # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
+ # this appears to be a python bug: http://bugs.python.org/issue3014
+ # (haven't seen this is quite some time... mysteriously resolved?)
+
+ torTools.NO_SPAWN = True # prevents further worker threads from being spawned
+
+ # stops panel daemons
+ panels["header"].stop()
+ panels["conn"].stop()
+ panels["log"].stop()
+
+ panels["header"].join()
+ panels["conn"].join()
+ panels["log"].join()
+
+ # joins on utility daemon threads - this might take a moment since
+ # the internal threadpools being joined might be sleeping
+ conn = torTools.getConn()
+ myPid = conn.getMyPid()
+
+ resourceTracker = sysTools.getResourceTracker(myPid) if (myPid and sysTools.isTrackerAlive(myPid)) else None
+ resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
+ if resourceTracker: resourceTracker.stop()
+ if resolver: resolver.stop() # sets halt flag (returning immediately)
+ hostnames.stop() # halts and joins on hostname worker thread pool
+ if resourceTracker: resourceTracker.join()
+ if resolver: resolver.join() # joins on halted resolver
+
+ conn.close() # joins on TorCtl event thread
+ break
+ elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
+ # switch page
+ if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+ else: page = (page + 1) % len(PAGES)
+
+ # skip connections listing if it's disabled
+ if page == 1 and isBlindMode:
+ if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+ else: page = (page + 1) % len(PAGES)
+
+ # pauses panels that aren't visible to prevent events from accumilating
+ # (otherwise they'll wait on the curses lock which might get demanding)
+ setPauseState(panels, isPaused, page)
+
+ # prevents panels on other pages from redrawing
+ for i in range(len(PAGES)):
+ isVisible = i == page
+ for entry in PAGES[i]: panels[entry].setVisible(isVisible)
+
+ panels["control"].page = page + 1
+
+ # TODO: this redraw doesn't seem necessary (redraws anyway after this
+ # loop) - look into this when refactoring
+ panels["control"].redraw(True)
+
+ selectiveRefresh(panels, page)
+ elif key == ord('p') or key == ord('P'):
+ # toggles update freezing
+ panel.CURSES_LOCK.acquire()
+ try:
+ isPaused = not isPaused
+ setPauseState(panels, isPaused, page)
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ finally:
+ panel.CURSES_LOCK.release()
+
+ selectiveRefresh(panels, page)
+ elif key == ord('x') or key == ord('X'):
+ # provides prompt to confirm that arm should issue a sighup
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # provides prompt
+ panels["control"].setMsg("This will reset Tor's internal state. Are you sure (x again to confirm)?", curses.A_BOLD)
+ panels["control"].redraw(True)
+
+ curses.cbreak()
+ confirmationKey = stdscr.getch()
+ if confirmationKey in (ord('x'), ord('X')):
+ try:
+ torTools.getConn().reload()
+ except IOError, exc:
+ log.log(log.ERR, "Error detected when reloading tor: %s" % sysTools.getFileErrorMsg(exc))
+
+ #errorMsg = " (%s)" % str(err) if str(err) else ""
+ #panels["control"].setMsg("Sighup failed%s" % errorMsg, curses.A_STANDOUT)
+ #panels["control"].redraw(True)
+ #time.sleep(2)
+
+ # reverts display settings
+ curses.halfdelay(REFRESH_RATE * 10)
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ setPauseState(panels, isPaused, page)
+ finally:
+ panel.CURSES_LOCK.release()
+ elif key == ord('h') or key == ord('H'):
+ # displays popup for current page's controls
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # lists commands
+ popup = panels["popup"]
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Page %i Commands:" % (page + 1), curses.A_STANDOUT)
+
+ pageOverrideKeys = ()
+
+ if page == 0:
+ graphedStats = panels["graph"].currentDisplay
+ if not graphedStats: graphedStats = "none"
+ popup.addfstr(1, 2, "<b>up arrow</b>: scroll log up a line")
+ popup.addfstr(1, 41, "<b>down arrow</b>: scroll log down a line")
+ popup.addfstr(2, 2, "<b>m</b>: increase graph size")
+ popup.addfstr(2, 41, "<b>n</b>: decrease graph size")
+ popup.addfstr(3, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
+ popup.addfstr(3, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
+ popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % panels["graph"].bounds.lower())
+ popup.addfstr(4, 41, "<b>a</b>: save snapshot of the log")
+ popup.addfstr(5, 2, "<b>e</b>: change logged events")
+
+ regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
+ popup.addfstr(5, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
+
+ hiddenEntryLabel = "visible" if panels["log"].showDuplicates else "hidden"
+ popup.addfstr(6, 2, "<b>u</b>: duplicate log entries (<b>%s</b>)" % hiddenEntryLabel)
+ popup.addfstr(6, 41, "<b>c</b>: clear event log")
+
+ pageOverrideKeys = (ord('m'), ord('n'), ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'), ord('x'))
+ if page == 1:
+ popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
+ popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
+ popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
+ popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
+
+ popup.addfstr(3, 2, "<b>enter</b>: edit configuration option")
+ popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
+
+ listingType = panels["conn"]._listingType.lower()
+ popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
+
+ popup.addfstr(4, 41, "<b>s</b>: sort ordering")
+
+ resolverUtil = connections.getResolver("tor").overwriteResolver
+ if resolverUtil == None: resolverUtil = "auto"
+ popup.addfstr(5, 2, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
+
+ pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('u'))
+ elif page == 2:
+ popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
+ popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
+ popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
+ popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
+
+ strippingLabel = "on" if panels["torrc"].stripComments else "off"
+ popup.addfstr(3, 2, "<b>s</b>: comment stripping (<b>%s</b>)" % strippingLabel)
+
+ lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
+ popup.addfstr(3, 41, "<b>n</b>: line numbering (<b>%s</b>)" % lineNumLabel)
+
+ popup.addfstr(4, 2, "<b>r</b>: reload torrc")
+ popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
+ elif page == 3:
+ popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
+ popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
+ popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
+ popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
+ popup.addfstr(3, 2, "<b>enter</b>: connection details")
+
+ popup.addstr(7, 2, "Press any key...")
+ popup.refresh()
+
+ # waits for user to hit a key, if it belongs to a command then executes it
+ curses.cbreak()
+ helpExitKey = stdscr.getch()
+ if helpExitKey in pageOverrideKeys: overrideKey = helpExitKey
+ curses.halfdelay(REFRESH_RATE * 10)
+
+ setPauseState(panels, isPaused, page)
+ selectiveRefresh(panels, page)
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 0 and (key == ord('s') or key == ord('S')):
+ # provides menu to pick stats to be graphed
+ #options = ["None"] + [label for label in panels["graph"].stats.keys()]
+ options = ["None"]
+
+ # appends stats labels with first letters of each word capitalized
+ initialSelection, i = -1, 1
+ if not panels["graph"].currentDisplay: initialSelection = 0
+ graphLabels = panels["graph"].stats.keys()
+ graphLabels.sort()
+ for label in graphLabels:
+ if label == panels["graph"].currentDisplay: initialSelection = i
+ words = label.split()
+ options.append(" ".join(word[0].upper() + word[1:] for word in words))
+ i += 1
+
+ # hides top label of the graph panel and pauses panels
+ if panels["graph"].currentDisplay:
+ panels["graph"].showLabel = False
+ panels["graph"].redraw(True)
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
+
+ # reverts changes made for popup
+ panels["graph"].showLabel = True
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1 and selection != initialSelection:
+ if selection == 0: panels["graph"].setStats(None)
+ else: panels["graph"].setStats(options[selection].lower())
+
+ selectiveRefresh(panels, page)
+
+ # TODO: this shouldn't be necessary with the above refresh, but doesn't seem responsive otherwise...
+ panels["graph"].redraw(True)
+ elif page == 0 and (key == ord('i') or key == ord('I')):
+ # provides menu to pick graph panel update interval
+ options = [label for (label, intervalTime) in graphing.graphPanel.UPDATE_INTERVALS]
+
+ initialSelection = panels["graph"].updateInterval
+
+ #initialSelection = -1
+ #for i in range(len(options)):
+ # if options[i] == panels["graph"].updateInterval: initialSelection = i
+
+ # hides top label of the graph panel and pauses panels
+ if panels["graph"].currentDisplay:
+ panels["graph"].showLabel = False
+ panels["graph"].redraw(True)
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
+
+ # reverts changes made for popup
+ panels["graph"].showLabel = True
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1: panels["graph"].updateInterval = selection
+
+ selectiveRefresh(panels, page)
+ elif page == 0 and (key == ord('b') or key == ord('B')):
+ # uses the next boundary type for graph
+ panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
+
+ selectiveRefresh(panels, page)
+ elif page == 0 and (key == ord('a') or key == ord('A')):
+ # allow user to enter a path to take a snapshot - abandons if left blank
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # provides prompt
+ panels["control"].setMsg("Path to save log snapshot: ")
+ panels["control"].redraw(True)
+
+ # gets user input (this blocks monitor updates)
+ pathInput = panels["control"].getstr(0, 27)
+
+ if pathInput:
+ try:
+ panels["log"].saveSnapshot(pathInput)
+ panels["control"].setMsg("Saved: %s" % pathInput, curses.A_STANDOUT)
+ panels["control"].redraw(True)
+ time.sleep(2)
+ except IOError, exc:
+ panels["control"].setMsg("Unable to save snapshot: %s" % sysTools.getFileErrorMsg(exc), curses.A_STANDOUT)
+ panels["control"].redraw(True)
+ time.sleep(2)
+
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ setPauseState(panels, isPaused, page)
+ finally:
+ panel.CURSES_LOCK.release()
+
+ panels["graph"].redraw(True)
+ elif page == 0 and (key == ord('e') or key == ord('E')):
+ # allow user to enter new types of events to log - unchanged if left blank
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # provides prompt
+ panels["control"].setMsg("Events to log: ")
+ panels["control"].redraw(True)
+
+ # lists event types
+ popup = panels["popup"]
+ popup.height = 11
+ popup.recreate(stdscr, 80)
+
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
+ lineNum = 1
+ for line in logPanel.EVENT_LISTING.split("\n"):
+ line = line[6:]
+ popup.addstr(lineNum, 1, line)
+ lineNum += 1
+ popup.refresh()
+
+ # gets user input (this blocks monitor updates)
+ eventsInput = panels["control"].getstr(0, 15)
+ if eventsInput: eventsInput = eventsInput.replace(' ', '') # strips spaces
+
+ # it would be nice to quit on esc, but looks like this might not be possible...
+ if eventsInput:
+ try:
+ expandedEvents = logPanel.expandEvents(eventsInput)
+ loggedEvents = setEventListening(expandedEvents, isBlindMode)
+ panels["log"].setLoggedEvents(loggedEvents)
+ except ValueError, exc:
+ panels["control"].setMsg("Invalid flags: %s" % str(exc), curses.A_STANDOUT)
+ panels["control"].redraw(True)
+ time.sleep(2)
+
+ # reverts popup dimensions
+ popup.height = 9
+ popup.recreate(stdscr, 80)
+
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ setPauseState(panels, isPaused, page)
+ finally:
+ panel.CURSES_LOCK.release()
+
+ panels["graph"].redraw(True)
+ elif page == 0 and (key == ord('f') or key == ord('F')):
+ # provides menu to pick previous regular expression filters or to add a new one
+ # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
+ options = ["None"] + regexFilters + ["New..."]
+ initialSelection = 0 if not panels["log"].regexFilter else 1
+
+ # hides top label of the graph panel and pauses panels
+ if panels["graph"].currentDisplay:
+ panels["graph"].showLabel = False
+ panels["graph"].redraw(True)
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
+
+ # applies new setting
+ if selection == 0:
+ panels["log"].setFilter(None)
+ elif selection == len(options) - 1:
+ # selected 'New...' option - prompt user to input regular expression
+ panel.CURSES_LOCK.acquire()
+ try:
+ # provides prompt
+ panels["control"].setMsg("Regular expression: ")
+ panels["control"].redraw(True)
+
+ # gets user input (this blocks monitor updates)
+ regexInput = panels["control"].getstr(0, 20)
+
+ if regexInput:
+ try:
+ panels["log"].setFilter(re.compile(regexInput))
+ if regexInput in regexFilters: regexFilters.remove(regexInput)
+ regexFilters = [regexInput] + regexFilters
+ except re.error, exc:
+ panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
+ panels["control"].redraw(True)
+ time.sleep(2)
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ finally:
+ panel.CURSES_LOCK.release()
+ elif selection != -1:
+ try:
+ panels["log"].setFilter(re.compile(regexFilters[selection - 1]))
+
+ # move selection to top
+ regexFilters = [regexFilters[selection - 1]] + regexFilters
+ del regexFilters[selection]
+ except re.error, exc:
+ # shouldn't happen since we've already checked validity
+ log.log(log.WARN, "Invalid regular expression ('%s': %s) - removing from listing" % (regexFilters[selection - 1], str(exc)))
+ del regexFilters[selection - 1]
+
+ if len(regexFilters) > MAX_REGEX_FILTERS: del regexFilters[MAX_REGEX_FILTERS:]
+
+ # reverts changes made for popup
+ panels["graph"].showLabel = True
+ setPauseState(panels, isPaused, page)
+ panels["graph"].redraw(True)
+ elif page == 0 and key in (ord('n'), ord('N'), ord('m'), ord('M')):
+ # Unfortunately modifier keys don't work with the up/down arrows (sending
+ # multiple keycodes. The only exception to this is shift + left/right,
+ # but for now just gonna use standard characters.
+
+ if key in (ord('n'), ord('N')):
+ panels["graph"].setGraphHeight(panels["graph"].graphHeight - 1)
+ else:
+ # don't grow the graph if it's already consuming the whole display
+ # (plus an extra line for the graph/log gap)
+ maxHeight = panels["graph"].parent.getmaxyx()[0] - panels["graph"].top
+ currentHeight = panels["graph"].getHeight()
+
+ if currentHeight < maxHeight + 1:
+ panels["graph"].setGraphHeight(panels["graph"].graphHeight + 1)
+ elif page == 0 and (key == ord('c') or key == ord('C')):
+ # provides prompt to confirm that arm should clear the log
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # provides prompt
+ panels["control"].setMsg("This will clear the log. Are you sure (c again to confirm)?", curses.A_BOLD)
+ panels["control"].redraw(True)
+
+ curses.cbreak()
+ confirmationKey = stdscr.getch()
+ if confirmationKey in (ord('c'), ord('C')): panels["log"].clear()
+
+ # reverts display settings
+ curses.halfdelay(REFRESH_RATE * 10)
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ setPauseState(panels, isPaused, page)
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 1 and (key == ord('u') or key == ord('U')):
+ # provides menu to pick identification resolving utility
+ options = ["auto"] + connections.Resolver.values()
+
+ currentOverwrite = connections.getResolver("tor").overwriteResolver # enums correspond to indices
+ if currentOverwrite == None: initialSelection = 0
+ else: initialSelection = options.index(currentOverwrite)
+
+ # hides top label of conn panel and pauses panels
+ panelTitle = panels["conn"]._title
+ panels["conn"]._title = ""
+ panels["conn"].redraw(True)
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
+ selectedOption = options[selection] if selection != "auto" else None
+
+ # reverts changes made for popup
+ panels["conn"]._title = panelTitle
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1 and selectedOption != connections.getResolver("tor").overwriteResolver:
+ connections.getResolver("tor").overwriteResolver = selectedOption
+ elif page == 1 and key in (ord('d'), ord('D')):
+ # presents popup for raw consensus data
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+ panelTitle = panels["conn"]._title
+ panels["conn"]._title = ""
+ panels["conn"].redraw(True)
+
+ descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, panels["conn"])
+
+ panels["conn"]._title = panelTitle
+ setPauseState(panels, isPaused, page)
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 1 and (key == ord('l') or key == ord('L')):
+ # provides a menu to pick the primary information we list connections by
+ options = cli.connections.entries.ListingType.values()
+
+ # dropping the HOSTNAME listing type until we support displaying that content
+ options.remove(cli.connections.entries.ListingType.HOSTNAME)
+
+ initialSelection = options.index(panels["conn"]._listingType)
+
+ # hides top label of connection panel and pauses the display
+ panelTitle = panels["conn"]._title
+ panels["conn"]._title = ""
+ panels["conn"].redraw(True)
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
+
+ # reverts changes made for popup
+ panels["conn"]._title = panelTitle
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1 and options[selection] != panels["conn"]._listingType:
+ panels["conn"].setListingType(options[selection])
+ panels["conn"].redraw(True)
+ elif page == 1 and (key == ord('s') or key == ord('S')):
+ # set ordering for connection options
+ titleLabel = "Connection Ordering:"
+ options = cli.connections.entries.SortAttr.values()
+ oldSelection = panels["conn"]._sortOrdering
+ optionColors = dict([(attr, cli.connections.entries.SORT_COLORS[attr]) for attr in options])
+ results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
+
+ if results:
+ panels["conn"].setSortOrder(results)
+
+ panels["conn"].redraw(True)
+ elif page == 2 and (key == ord('c') or key == ord('C')) and False:
+ # TODO: disabled for now (probably gonna be going with separate pages
+ # rather than popup menu)
+ # provides menu to pick config being displayed
+ #options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
+ options = []
+ initialSelection = panels["torrc"].configType
+
+ # hides top label of the graph panel and pauses panels
+ panels["torrc"].showLabel = False
+ panels["torrc"].redraw(True)
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Configuration:", options, initialSelection)
+
+ # reverts changes made for popup
+ panels["torrc"].showLabel = True
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1: panels["torrc"].setConfigType(selection)
+
+ selectiveRefresh(panels, page)
+ elif page == 2 and (key == ord('w') or key == ord('W')):
+ # display a popup for saving the current configuration
+ panel.CURSES_LOCK.acquire()
+ try:
+ configLines = torConfig.getCustomOptions(True)
+
+ # lists event types
+ popup = panels["popup"]
+ popup.height = len(configLines) + 3
+ popup.recreate(stdscr)
+ displayHeight, displayWidth = panels["popup"].getPreferredSize()
+
+ # displayed options (truncating the labels if there's limited room)
+ if displayWidth >= 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
+ lastIndex = min(displayHeight - 3, len(configLines) - 1)
+ isOptionLineSeparate = displayWidth < (30 + len(configLines[lastIndex]))
+
+ # if we're showing all the content and have room to display selection
+ # options besides the text then shrink the popup by a row
+ if not isOptionLineSeparate and displayHeight == len(configLines) + 3:
+ popup.height -= 1
+ popup.recreate(stdscr)
+
+ key, selection = 0, 2
+ while not uiTools.isSelectionKey(key):
+ # if the popup has been resized then recreate it (needed for the
+ # proper border height)
+ newHeight, newWidth = panels["popup"].getPreferredSize()
+ if (displayHeight, displayWidth) != (newHeight, newWidth):
+ displayHeight, displayWidth = newHeight, newWidth
+ popup.recreate(stdscr)
+
+ # if there isn't room to display the popup then cancel it
+ if displayHeight <= 2:
+ selection = 2
+ break
+
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
+
+ visibleConfigLines = displayHeight - 3 if isOptionLineSeparate else displayHeight - 2
+ for i in range(visibleConfigLines):
+ line = uiTools.cropStr(configLines[i], displayWidth - 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 'T' between the lower left and the covered panel's scroll bar
+ if displayWidth > 1: popup.win.addch(displayHeight - 1, 1, curses.ACS_TTEE)
+
+ # draws selection options (drawn right to left)
+ drawX = displayWidth - 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(displayHeight - 2, drawX, "[")
+ popup.addstr(displayHeight - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
+ popup.addstr(displayHeight - 2, drawX + len(optionLabel) + 1, "]")
+
+ drawX -= 1 # space gap between the options
+
+ popup.refresh()
+
+ key = stdscr.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 = torConfig.getTorrc()
+ try: configLocation = loadedTorrc.getConfigLocation()
+ except IOError: configLocation = ""
+
+ if selection == 1:
+ # prompts user for a configuration location
+ promptMsg = "Save to (esc to cancel): "
+ panels["control"].setMsg(promptMsg)
+ panels["control"].redraw(True)
+ configLocation = panels["control"].getstr(0, len(promptMsg), configLocation)
+ if configLocation: configLocation = os.path.abspath(configLocation)
+
+ if configLocation:
+ try:
+ # make dir if the path doesn't already exist
+ baseDir = os.path.dirname(configLocation)
+ if not os.path.exists(baseDir): os.makedirs(baseDir)
+
+ # saves the configuration to the file
+ configFile = open(configLocation, "w")
+ configFile.write("\n".join(configLines))
+ configFile.close()
+
+ # reloads the cached torrc if overwriting it
+ if configLocation == loadedTorrc.getConfigLocation():
+ try:
+ loadedTorrc.load()
+ panels["torrc"]._lastContentHeightArgs = None
+ except IOError: pass
+
+ msg = "Saved configuration to %s" % configLocation
+ except (IOError, OSError), exc:
+ msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
+
+ panels["control"].setMsg(msg, curses.A_STANDOUT)
+ panels["control"].redraw(True)
+ time.sleep(2)
+
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+
+ # reverts popup dimensions
+ popup.height = 9
+ popup.recreate(stdscr, 80)
+ finally:
+ panel.CURSES_LOCK.release()
+
+ panels["config"].redraw(True)
+ elif page == 2 and (key == ord('s') or key == ord('S')):
+ # set ordering for config options
+ titleLabel = "Config Option Ordering:"
+ options = [configPanel.FIELD_ATTR[field][0] for field in configPanel.Field.values()]
+ oldSelection = [configPanel.FIELD_ATTR[field][0] for field in panels["config"].sortOrdering]
+ optionColors = dict([configPanel.FIELD_ATTR[field] for field in configPanel.Field.values()])
+ results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
+
+ if results:
+ # converts labels back to enums
+ resultEnums = []
+
+ for label in results:
+ for entryEnum in configPanel.FIELD_ATTR:
+ if label == configPanel.FIELD_ATTR[entryEnum][0]:
+ resultEnums.append(entryEnum)
+ break
+
+ panels["config"].setSortOrder(resultEnums)
+
+ panels["config"].redraw(True)
+ elif page == 2 and uiTools.isSelectionKey(key):
+ # let the user edit the configuration value, unchanged if left blank
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # provides prompt
+ selection = panels["config"].getSelection()
+ configOption = selection.get(configPanel.Field.OPTION)
+ titleMsg = "%s Value (esc to cancel): " % configOption
+ panels["control"].setMsg(titleMsg)
+ panels["control"].redraw(True)
+
+ displayWidth = panels["control"].getPreferredSize()[1]
+ initialValue = selection.get(configPanel.Field.VALUE)
+
+ # initial input for the text field
+ initialText = ""
+ if CONFIG["features.config.prepopulateEditValues"] and initialValue != "<none>":
+ initialText = initialValue
+
+ newConfigValue = panels["control"].getstr(0, len(titleMsg), initialText)
+
+ # it would be nice to quit on esc, but looks like this might not be possible...
+ if newConfigValue != None and newConfigValue != initialValue:
+ conn = torTools.getConn()
+
+ # if the value's a boolean then allow for 'true' and 'false' inputs
+ if selection.get(configPanel.Field.TYPE) == "Boolean":
+ if newConfigValue.lower() == "true": newConfigValue = "1"
+ elif newConfigValue.lower() == "false": newConfigValue = "0"
+
+ try:
+ if selection.get(configPanel.Field.TYPE) == "LineList":
+ newConfigValue = newConfigValue.split(",")
+
+ conn.setOption(configOption, newConfigValue)
+
+ # resets the isDefault flag
+ customOptions = torConfig.getCustomOptions()
+ selection.fields[configPanel.Field.IS_DEFAULT] = not configOption in customOptions
+
+ panels["config"].redraw(True)
+ except Exception, exc:
+ errorMsg = "%s (press any key)" % exc
+ panels["control"].setMsg(uiTools.cropStr(errorMsg, displayWidth), curses.A_STANDOUT)
+ panels["control"].redraw(True)
+
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+ stdscr.getch()
+ curses.halfdelay(REFRESH_RATE * 10)
+
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ setPauseState(panels, isPaused, page)
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 3 and key == ord('r') or key == ord('R'):
+ # reloads torrc, providing a notice if successful or not
+ loadedTorrc = torConfig.getTorrc()
+ loadedTorrc.getLock().acquire()
+
+ try:
+ loadedTorrc.load()
+ isSuccessful = True
+ except IOError:
+ isSuccessful = False
+
+ loadedTorrc.getLock().release()
+
+ #isSuccessful = panels["torrc"].loadConfig(logErrors = False)
+ #confTypeLabel = confPanel.CONFIG_LABELS[panels["torrc"].configType]
+ resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
+ if isSuccessful:
+ panels["torrc"]._lastContentHeightArgs = None
+ panels["torrc"].redraw(True)
+
+ panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
+ panels["control"].redraw(True)
+ time.sleep(1)
+
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ elif page == 0:
+ panels["log"].handleKey(key)
+ elif page == 1:
+ panels["conn"].handleKey(key)
+ elif page == 2:
+ panels["config"].handleKey(key)
+ elif page == 3:
+ panels["torrc"].handleKey(key)
+
+def startTorMonitor(startTime, loggedEvents, isBlindMode):
+ try:
+ curses.wrapper(drawTorMonitor, startTime, loggedEvents, isBlindMode)
+ except KeyboardInterrupt:
+ pass # skip printing stack trace in case of keyboard interrupt
+
diff --git a/src/cli/descriptorPopup.py b/src/cli/descriptorPopup.py
new file mode 100644
index 0000000..cdc959d
--- /dev/null
+++ b/src/cli/descriptorPopup.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+# descriptorPopup.py -- popup panel used to show raw consensus data
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import math
+import socket
+import curses
+from TorCtl import TorCtl
+
+import controller
+import connections.connEntry
+from util import panel, torTools, uiTools
+
+# field keywords used to identify areas for coloring
+LINE_NUM_COLOR = "yellow"
+HEADER_COLOR = "cyan"
+HEADER_PREFIX = ["ns/id/", "desc/id/"]
+
+SIG_COLOR = "red"
+SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
+SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
+
+UNRESOLVED_MSG = "No consensus data available"
+ERROR_MSG = "Unable to retrieve data"
+
+class PopupProperties:
+ """
+ State attributes of popup window for consensus descriptions.
+ """
+
+ def __init__(self):
+ self.fingerprint = ""
+ self.entryColor = "white"
+ self.text = []
+ self.scroll = 0
+ self.showLineNum = True
+
+ def reset(self, fingerprint, entryColor):
+ self.fingerprint = fingerprint
+ self.entryColor = entryColor
+ self.text = []
+ self.scroll = 0
+
+ if fingerprint == "UNKNOWN":
+ self.fingerprint = None
+ self.showLineNum = False
+ self.text.append(UNRESOLVED_MSG)
+ else:
+ conn = torTools.getConn()
+
+ try:
+ self.showLineNum = True
+ self.text.append("ns/id/%s" % fingerprint)
+ self.text += conn.getConsensusEntry(fingerprint).split("\n")
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+ self.text = self.text + [ERROR_MSG, ""]
+
+ try:
+ descCommand = "desc/id/%s" % fingerprint
+ self.text.append("desc/id/%s" % fingerprint)
+ self.text += conn.getDescriptorEntry(fingerprint).split("\n")
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+ self.text = self.text + [ERROR_MSG]
+
+ def handleKey(self, key, height):
+ if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
+ elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.text) - height))
+ elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - height, 0)
+ elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + height, len(self.text) - height))
+
+def showDescriptorPopup(popup, stdscr, connectionPanel):
+ """
+ Presents consensus descriptor in popup window with the following controls:
+ Up, Down, Page Up, Page Down - scroll descriptor
+ Right, Left - next / previous connection
+ Enter, Space, d, D - close popup
+ """
+
+ properties = PopupProperties()
+ isVisible = True
+
+ if not panel.CURSES_LOCK.acquire(False): return
+ try:
+ while isVisible:
+ selection = connectionPanel._scroller.getCursorSelection(connectionPanel._entryLines)
+ if not selection: break
+ fingerprint = selection.foreign.getFingerprint()
+ entryColor = connections.connEntry.CATEGORY_COLOR[selection.getType()]
+ properties.reset(fingerprint, entryColor)
+
+ # constrains popup size to match text
+ width, height = 0, 0
+ for line in properties.text:
+ # width includes content, line number field, and border
+ lineWidth = len(line) + 5
+ if properties.showLineNum: lineWidth += int(math.log10(len(properties.text))) + 1
+ width = max(width, lineWidth)
+
+ # tracks number of extra lines that will be taken due to text wrap
+ height += (lineWidth - 2) / connectionPanel.maxX
+
+ popup.setHeight(min(len(properties.text) + height + 2, connectionPanel.maxY))
+ popup.recreate(stdscr, width)
+
+ while isVisible:
+ draw(popup, properties)
+ key = stdscr.getch()
+
+ if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')):
+ # closes popup
+ isVisible = False
+ elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
+ # navigation - pass on to connPanel and recreate popup
+ connectionPanel.handleKey(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN)
+ break
+ else: properties.handleKey(key, popup.height - 2)
+
+ popup.setHeight(9)
+ popup.recreate(stdscr, 80)
+ finally:
+ panel.CURSES_LOCK.release()
+
+def draw(popup, properties):
+ popup.clear()
+ popup.win.box()
+ xOffset = 2
+
+ if properties.text:
+ if properties.fingerprint: popup.addstr(0, 0, "Consensus Descriptor (%s):" % properties.fingerprint, curses.A_STANDOUT)
+ else: popup.addstr(0, 0, "Consensus Descriptor:", curses.A_STANDOUT)
+
+ isEncryption = False # true if line is part of an encryption block
+
+ # checks if first line is in an encryption block
+ for i in range(0, properties.scroll):
+ lineText = properties.text[i].strip()
+ if lineText in SIG_START_KEYS: isEncryption = True
+ elif lineText in SIG_END_KEYS: isEncryption = False
+
+ pageHeight = popup.maxY - 2
+ numFieldWidth = int(math.log10(len(properties.text))) + 1
+ lineNum = 1
+ for i in range(properties.scroll, min(len(properties.text), properties.scroll + pageHeight)):
+ lineText = properties.text[i].strip()
+
+ numOffset = 0 # offset for line numbering
+ if properties.showLineNum:
+ popup.addstr(lineNum, xOffset, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR))
+ numOffset = numFieldWidth + 1
+
+ if lineText:
+ keyword = lineText.split()[0] # first word of line
+ remainder = lineText[len(keyword):]
+ keywordFormat = curses.A_BOLD | uiTools.getColor(properties.entryColor)
+ remainderFormat = uiTools.getColor(properties.entryColor)
+
+ if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]):
+ keyword, remainder = lineText, ""
+ keywordFormat = curses.A_BOLD | uiTools.getColor(HEADER_COLOR)
+ if lineText == UNRESOLVED_MSG or lineText == ERROR_MSG:
+ keyword, remainder = lineText, ""
+ if lineText in SIG_START_KEYS:
+ keyword, remainder = lineText, ""
+ isEncryption = True
+ keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
+ elif lineText in SIG_END_KEYS:
+ keyword, remainder = lineText, ""
+ isEncryption = False
+ keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
+ elif isEncryption:
+ keyword, remainder = lineText, ""
+ keywordFormat = uiTools.getColor(SIG_COLOR)
+
+ lineNum, xLoc = controller.addstr_wrap(popup, lineNum, 0, keyword, keywordFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
+ lineNum, xLoc = controller.addstr_wrap(popup, lineNum, xLoc, remainder, remainderFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
+
+ lineNum += 1
+ if lineNum > pageHeight: break
+
+ popup.refresh()
+
diff --git a/src/cli/graphing/__init__.py b/src/cli/graphing/__init__.py
new file mode 100644
index 0000000..2dddaa3
--- /dev/null
+++ b/src/cli/graphing/__init__.py
@@ -0,0 +1,6 @@
+"""
+Graphing panel resources.
+"""
+
+__all__ = ["graphPanel", "bandwidthStats", "connStats", "resourceStats"]
+
diff --git a/src/cli/graphing/bandwidthStats.py b/src/cli/graphing/bandwidthStats.py
new file mode 100644
index 0000000..2864dd8
--- /dev/null
+++ b/src/cli/graphing/bandwidthStats.py
@@ -0,0 +1,398 @@
+"""
+Tracks bandwidth usage of the tor process, expanding to include accounting
+stats if they're set.
+"""
+
+import time
+
+from cli.graphing import graphPanel
+from util import log, sysTools, torTools, uiTools
+
+DL_COLOR, UL_COLOR = "green", "cyan"
+
+# width at which panel abandons placing optional stats (avg and total) with
+# header in favor of replacing the x-axis label
+COLLAPSE_WIDTH = 135
+
+# valid keys for the accountingInfo mapping
+ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writtenLimit")
+
+PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file"
+PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
+
+DEFAULT_CONFIG = {"features.graph.bw.transferInBytes": False,
+ "features.graph.bw.accounting.show": True,
+ "features.graph.bw.accounting.rate": 10,
+ "features.graph.bw.accounting.isTimeLong": False,
+ "log.graph.bw.prepopulateSuccess": log.NOTICE,
+ "log.graph.bw.prepopulateFailure": log.NOTICE}
+
+class BandwidthStats(graphPanel.GraphStats):
+ """
+ Uses tor BW events to generate bandwidth usage graph.
+ """
+
+ def __init__(self, config=None):
+ graphPanel.GraphStats.__init__(self)
+
+ self._config = dict(DEFAULT_CONFIG)
+ if config:
+ config.update(self._config, {"features.graph.bw.accounting.rate": 1})
+
+ # stats prepopulated from tor's state file
+ self.prepopulatePrimaryTotal = 0
+ self.prepopulateSecondaryTotal = 0
+ 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
+ self.resetListener(conn, torTools.State.INIT) # 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")
+ if readTotal and readTotal.isdigit():
+ self.initialPrimaryTotal = int(readTotal) / 1024 # Bytes -> KB
+
+ writeTotal = conn.getInfo("traffic/written")
+ if writeTotal and writeTotal.isdigit():
+ self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
+
+ def resetListener(self, conn, 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 == torTools.State.INIT and self._config["features.graph.bw.accounting.show"]:
+ self.isAccounting = conn.getInfo('accounting/enabled') == '1'
+
+ 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")
+ if orPort == "0": return
+
+ # gets the uptime (using the same parameters as the header panel to take
+ # advantage of caching
+ uptime = None
+ queryPid = conn.getMyPid()
+ if queryPid:
+ queryParam = ["%cpu", "rss", "%mem", "etime"]
+ queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
+ psCall = sysTools.call(queryCmd, 3600, True)
+
+ 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
+ if not uptime or not "-" in uptime:
+ msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
+ log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+ return False
+
+ # get the user's data directory (usually '~/.tor')
+ dataDir = conn.getOption("DataDirectory")
+ if not dataDir:
+ msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
+ log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+ return False
+
+ # attempt to open the state file
+ try: stateFile = open("%s%s/state" % (conn.getPathPrefix(), dataDir), "r")
+ except IOError:
+ msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
+ log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+ 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]
+ bwReadEntries.pop()
+ elif line.startswith("BWHistoryWriteValues"):
+ bwWriteEntries = line[21:].split(",")
+ bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
+ bwWriteEntries.pop()
+ elif line.startswith("BWHistoryReadEnds"):
+ lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
+ lastReadTime -= 900
+ missingReadEntries = int((time.time() - lastReadTime) / 900)
+ elif line.startswith("BWHistoryWriteEnds"):
+ 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.log(self._config["log.graph.bw.prepopulateFailure"], 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)" % uiTools.getTimeLabel(missingSec, 0, True)
+ log.log(self._config["log.graph.bw.prepopulateSuccess"], msg)
+
+ return True
+
+ def bandwidth_event(self, event):
+ if self.isAccounting and self.isNextTickRedraw():
+ if time.time() - self.accountingLastUpdated >= self._config["features.graph.bw.accounting.rate"]:
+ 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.addfstr(labelingLine + 2, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
+
+ 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.addfstr(labelingLine + 2, 0, "<b>Accounting:</b> 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" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"]))
+
+ # drops label's components if there's not enough space
+ labeling = graphType + " (" + "".join(stats).strip() + "):"
+ while len(labeling) >= width:
+ if len(stats) > 1:
+ del stats[-1]
+ labeling = graphType + " (" + "".join(stats).strip() + "):"
+ 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")
+ if not self._titleStats or not myFingerprint or (event and myFingerprint in event.idlist):
+ stats = []
+ bwRate = conn.getMyBandwidthRate()
+ bwBurst = conn.getMyBandwidthBurst()
+ bwObserved = conn.getMyBandwidthObserved()
+ bwMeasured = conn.getMyBandwidthMeasured()
+ labelInBytes = self._config["features.graph.bw.transferInBytes"]
+
+ if bwRate and bwBurst:
+ bwRateLabel = uiTools.getSizeLabel(bwRate, 1, False, labelInBytes)
+ bwBurstLabel = uiTools.getSizeLabel(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).
+ if bwObserved and (not bwMeasured or bwMeasured == bwObserved):
+ stats.append("observed: %s/s" % uiTools.getSizeLabel(bwObserved, 1, False, labelInBytes))
+ elif bwMeasured:
+ stats.append("measured: %s/s" % uiTools.getSizeLabel(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" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, self._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" % uiTools.getSizeLabel(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")
+
+ # provides a nicely formatted reset time
+ endInterval = conn.getInfo("accounting/interval-end")
+ 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 self._config["features.graph.bw.accounting.isTimeLong"]:
+ queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True))
+ else:
+ days = sec / 86400
+ sec %= 86400
+ hours = sec / 3600
+ sec %= 3600
+ 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")
+ left = conn.getInfo("accounting/bytes-left")
+
+ 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"] = uiTools.getSizeLabel(read)
+ queried["written"] = uiTools.getSizeLabel(written)
+ queried["readLimit"] = uiTools.getSizeLabel(read + readLeft)
+ queried["writtenLimit"] = uiTools.getSizeLabel(written + writtenLeft)
+
+ self.accountingInfo = queried
+ self.accountingLastUpdated = time.time()
+
diff --git a/src/cli/graphing/connStats.py b/src/cli/graphing/connStats.py
new file mode 100644
index 0000000..51227b7
--- /dev/null
+++ b/src/cli/graphing/connStats.py
@@ -0,0 +1,54 @@
+"""
+Tracks stats concerning tor's current connections.
+"""
+
+from cli.graphing import graphPanel
+from util import connections, torTools
+
+class ConnStats(graphPanel.GraphStats):
+ """
+ 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, torTools.State.INIT) # initialize port values
+ conn.addStatusListener(self.resetListener)
+
+ def resetListener(self, conn, eventType):
+ if eventType == torTools.State.INIT:
+ self.orPort = conn.getOption("ORPort", "0")
+ self.dirPort = conn.getOption("DirPort", "0")
+ self.controlPort = conn.getOption("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/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
new file mode 100644
index 0000000..e4b493d
--- /dev/null
+++ b/src/cli/graphing/graphPanel.py
@@ -0,0 +1,407 @@
+"""
+Flexible panel for presenting bar graphs for a variety of stats. This panel is
+just concerned with the rendering of information, which is actually collected
+and stored by implementations of the GraphStats interface. Panels are made up
+of a title, followed by headers and graphs for two sets of stats. For
+instance...
+
+Bandwidth (cap: 5 MB, burst: 10 MB):
+Downloaded (0.0 B/sec): Uploaded (0.0 B/sec):
+ 34 30
+ * *
+ ** * * * **
+ * * * ** ** ** *** ** ** ** **
+ ********* ****** ****** ********* ****** ******
+ 0 ************ **************** 0 ************ ****************
+ 25s 50 1m 1.6 2.0 25s 50 1m 1.6 2.0
+"""
+
+import copy
+import curses
+from TorCtl import TorCtl
+
+from util import enum, panel, uiTools
+
+# time intervals at which graphs can be updated
+UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5), ("30 seconds", 30),
+ ("minutely", 60), ("15 minute", 900), ("30 minute", 1800),
+ ("hourly", 3600), ("daily", 86400)]
+
+DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
+DEFAULT_COLOR_PRIMARY, DEFAULT_COLOR_SECONDARY = "green", "cyan"
+MIN_GRAPH_HEIGHT = 1
+
+# enums for graph bounds:
+# Bounds.GLOBAL_MAX - global maximum (highest value ever seen)
+# Bounds.LOCAL_MAX - local maximum (highest value currently on the graph)
+# Bounds.TIGHT - local maximum and minimum
+Bounds = enum.Enum("GLOBAL_MAX", "LOCAL_MAX", "TIGHT")
+
+WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
+
+# used for setting defaults when initializing GraphStats and GraphPanel instances
+CONFIG = {"features.graph.height": 7,
+ "features.graph.interval": 0,
+ "features.graph.bound": 1,
+ "features.graph.maxWidth": 150,
+ "features.graph.showIntermediateBounds": True}
+
+def loadConfig(config):
+ config.update(CONFIG, {
+ "features.graph.height": MIN_GRAPH_HEIGHT,
+ "features.graph.maxWidth": 1,
+ "features.graph.interval": (0, len(UPDATE_INTERVALS) - 1),
+ "features.graph.bound": (0, 2)})
+
+class GraphStats(TorCtl.PostEventListener):
+ """
+ Module that's expected to update dynamically and provide attributes to be
+ 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, isPauseBuffer=False):
+ """
+ Initializes parameters needed to present a graph.
+ """
+
+ TorCtl.PostEventListener.__init__(self)
+
+ # panel to be redrawn when updated (set when added to GraphPanel)
+ self._graphPanel = None
+
+ # mirror instance used to track updates when paused
+ self.isPaused, self.isPauseBuffer = False, isPauseBuffer
+ if isPauseBuffer: self._pauseBuffer = None
+ else: self._pauseBuffer = GraphStats(True)
+
+ # 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]
+
+ 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 not self.isPauseBuffer and not self.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 setPaused(self, isPause):
+ """
+ If true, prevents bandwidth updates from being presented. This is a no-op
+ if a pause buffer.
+ """
+
+ if isPause == self.isPaused or self.isPauseBuffer: return
+ self.isPaused = isPause
+
+ if self.isPaused: active, inactive = self._pauseBuffer, self
+ else: active, inactive = self, self._pauseBuffer
+ self._parameterSwap(active, inactive)
+
+ def bandwidth_event(self, event):
+ self.eventTick()
+
+ def _parameterSwap(self, active, inactive):
+ """
+ Either overwrites parameters of pauseBuffer or with the current values or
+ vice versa. This is a helper method for setPaused and should be overwritten
+ to append with additional parameters that need to be preserved when paused.
+ """
+
+ # The pause buffer is constructed as a GraphStats instance which will
+ # become problematic if this is overridden by any implementations (which
+ # currently isn't the case). If this happens then the pause buffer will
+ # need to be of the requester's type (not quite sure how to do this
+ # gracefully...).
+
+ active.tick = inactive.tick
+ active.lastPrimary = inactive.lastPrimary
+ active.lastSecondary = inactive.lastSecondary
+ active.primaryTotal = inactive.primaryTotal
+ active.secondaryTotal = inactive.secondaryTotal
+ active.maxPrimary = dict(inactive.maxPrimary)
+ active.maxSecondary = dict(inactive.maxSecondary)
+ active.primaryCounts = copy.deepcopy(inactive.primaryCounts)
+ active.secondaryCounts = copy.deepcopy(inactive.secondaryCounts)
+
+ def _processEvent(self, primary, secondary):
+ """
+ Includes new stats in graphs and notifies associated GraphPanel of changes.
+ """
+
+ if self.isPaused: self._pauseBuffer._processEvent(primary, secondary)
+ else:
+ 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: self._graphPanel.redraw(True)
+
+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"]
+ self.bounds = Bounds.values()[CONFIG["features.graph.bound"]]
+ self.graphHeight = CONFIG["features.graph.height"]
+ self.currentDisplay = None # label of the stats currently being displayed
+ self.stats = {} # available stats (mappings of label -> instance)
+ self.showLabel = True # shows top label if true, hides otherwise
+ self.isPaused = False
+
+ 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 draw(self, width, height):
+ """ Redraws graph panel """
+
+ if self.currentDisplay:
+ param = self.stats[self.currentDisplay]
+ graphCol = min((width - 10) / 2, param.maxCol)
+
+ primaryColor = uiTools.getColor(param.getColor(True))
+ secondaryColor = uiTools.getColor(param.getColor(False))
+
+ if self.showLabel: 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])
+ secondaryMaxBound = int(param.maxSecondary[self.updateInterval])
+ else:
+ # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
+ if graphCol < 2:
+ # nothing being displayed
+ primaryMaxBound, secondaryMaxBound = 0, 0
+ 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 - 1) * (self.graphHeight - row - 1)
+ if not primaryVal in (primaryMinBound, primaryMaxBound): self.addstr(row + 2, 0, "%4i" % primaryVal, primaryColor)
+
+ if secondaryMinBound != secondaryMaxBound:
+ secondaryVal = (secondaryMaxBound - secondaryMinBound) / (self.graphHeight - 1) * (self.graphHeight - row - 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 = uiTools.getTimeLabel(loc * intervalSec, decimalPrecision)
+
+ if not unitsLabel: unitsLabel = timeLabel[-1]
+ elif unitsLabel != timeLabel[-1]:
+ # upped scale so also up precision of future measurements
+ unitsLabel = timeLabel[-1]
+ decimalPrecision += 1
+ 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
+ stats.isPaused = True
+ self.stats[label] = stats
+
+ 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].setPaused(True)
+
+ if not label:
+ self.currentDisplay = None
+ elif label in self.stats.keys():
+ self.currentDisplay = label
+ self.stats[label].setPaused(self.isPaused)
+ else: raise ValueError("Unrecognized stats label: %s" % label)
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents bandwidth updates from being presented.
+ """
+
+ if isPause == self.isPaused: return
+ self.isPaused = isPause
+ if self.currentDisplay: self.stats[self.currentDisplay].setPaused(self.isPaused)
+
diff --git a/src/cli/graphing/resourceStats.py b/src/cli/graphing/resourceStats.py
new file mode 100644
index 0000000..f26d5c1
--- /dev/null
+++ b/src/cli/graphing/resourceStats.py
@@ -0,0 +1,47 @@
+"""
+Tracks the system resource usage (cpu and memory) of the tor process.
+"""
+
+from cli.graphing import graphPanel
+from util import sysTools, torTools, uiTools
+
+class ResourceStats(graphPanel.GraphStats):
+ """
+ System resource usage tracker.
+ """
+
+ def __init__(self):
+ graphPanel.GraphStats.__init__(self)
+ self.queryPid = torTools.getConn().getMyPid()
+
+ 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:
+ # memory sizes are converted from MB to B before generating labels
+ usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
+ avgLabel = uiTools.getSizeLabel(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)
+
+ if 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/src/cli/headerPanel.py b/src/cli/headerPanel.py
new file mode 100644
index 0000000..f653299
--- /dev/null
+++ b/src/cli/headerPanel.py
@@ -0,0 +1,474 @@
+"""
+Top panel for every page, containing basic system and tor related information.
+If there's room available then this expands to present its information in two
+columns, otherwise it's laid out as follows:
+ arm - <hostname> (<os> <sys/version>) Tor <tor/version> (<new, old, recommended, etc>)
+ <nickname> - <address>:<orPort>, [Dir Port: <dirPort>, ]Control Port (<open, password, cookie>): <controlPort>
+ cpu: <cpu%> mem: <mem> (<mem%>) uid: <uid> uptime: <upmin>:<upsec>
+ fingerprint: <fingerprint>
+
+Example:
+ arm - odin (Linux 2.6.24-24-generic) Tor 0.2.1.19 (recommended)
+ odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
+ cpu: 14.6% mem: 42 MB (4.2%) pid: 20060 uptime: 48:27
+ fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
+"""
+
+import os
+import time
+import curses
+import threading
+
+from util import log, panel, sysTools, torTools, uiTools
+
+# minimum width for which panel attempts to double up contents (two columns to
+# better use screen real estate)
+MIN_DUAL_COL_WIDTH = 141
+
+FLAG_COLORS = {"Authority": "white", "BadExit": "red", "BadDirectory": "red", "Exit": "cyan",
+ "Fast": "yellow", "Guard": "green", "HSDir": "magenta", "Named": "blue",
+ "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",
+ "old": "red", "unrecommended": "red", "unknown": "cyan"}
+
+DEFAULT_CONFIG = {"features.showFdUsage": False,
+ "log.fdUsageSixtyPercent": log.NOTICE,
+ "log.fdUsageNinetyPercent": log.WARN}
+
+class HeaderPanel(panel.Panel, threading.Thread):
+ """
+ Top area contenting tor settings and system information. Stats are stored in
+ the vals mapping, keys including:
+ tor/ version, versionStatus, nickname, orPort, dirPort, controlPort,
+ exitPolicy, isAuthPassword (bool), isAuthCookie (bool),
+ orListenAddr, *address, *fingerprint, *flags, pid, startTime,
+ *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, config = None):
+ panel.Panel.__init__(self, stdscr, "header", 0)
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+
+ self._config = dict(DEFAULT_CONFIG)
+ if config: config.update(self._config)
+
+ self._isTorConnected = True
+ self._lastUpdate = -1 # time the content was last revised
+ self._isPaused = False # prevents updates if true
+ 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 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:
+ versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \
+ self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white"
+ versionStatusMsg = "<%s>%s</%s>" % (versionColor, self.vals["tor/versionStatus"], versionColor)
+ self.addfstr(0, 43, "Tor %s (%s)" % (self.vals["tor/version"], versionStatusMsg))
+ 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)
+ 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
+ entry = ""
+ 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):
+ if len(entry) + len(label) <= leftWidth: entry += label
+ else: break
+ else:
+ # non-relay (client only)
+ # TODO: not sure what sort of stats to provide...
+ entry = "<red><b>Relaying Disabled</b></red>"
+
+ if self.vals["tor/isAuthPassword"]: authType = "password"
+ elif self.vals["tor/isAuthCookie"]: authType = "cookie"
+ else: authType = "open"
+
+ if len(entry) + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth:
+ authColor = "red" if authType == "open" else "green"
+ authLabel = "<%s>%s</%s>" % (authColor, authType, authColor)
+ self.addfstr(1, 0, "%s, Control Port (%s): %s" % (entry, authLabel, self.vals["tor/controlPort"]))
+ elif len(entry) + 16 + len(self.vals["tor/controlPort"]) <= leftWidth:
+ self.addstr(1, 0, "%s, Control Port: %s" % (entry, self.vals["tor/controlPort"]))
+ else: self.addstr(1, 0, entry)
+
+ # Line 3 / Line 1 Right (system usage info)
+ y, x = (0, leftWidth) if isWide else (2, 0)
+ if self.vals["stat/rss"] != "0": memoryLabel = uiTools.getSizeLabel(int(self.vals["stat/rss"]))
+ else: memoryLabel = "0"
+
+ uptimeLabel = ""
+ if self.vals["tor/startTime"]:
+ if self._haltTime:
+ # freeze the uptime when paused or the tor process is stopped
+ uptimeLabel = uiTools.getShortTimeLabel(self._haltTime - self.vals["tor/startTime"])
+ else:
+ uptimeLabel = uiTools.getShortTimeLabel(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 self._config["features.showFdUsage"]:
+ fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL
+ if fdPercent >= 95:
+ fdPercentFormat = curses.A_BOLD | uiTools.getColor("red")
+ elif fdPercent >= 90:
+ 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:
+ flagLine = "flags: "
+ for flag in self.vals["tor/flags"]:
+ flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white"
+ flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor)
+
+ if len(self.vals["tor/flags"]) > 0: flagLine = flagLine[:-2]
+ else: flagLine += "<b><cyan>none</cyan></b>"
+
+ self.addfstr(2 if isWide else 4, 0, flagLine)
+ else:
+ statusTime = torTools.getConn().getStatus()[1]
+ statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime))
+ self.addfstr(2 if isWide else 4, 0, "<b><red>Tor Disconnected</red></b> (%s)" % 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>"
+
+ # 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()
+ displayedPolicy = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy
+ if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy
+ elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy
+ elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy
+ policies[i] = policy
+
+ self.addfstr(2, leftWidth, "exit policy: %s" % ", ".join(policies))
+ else:
+ # Client only
+ # TODO: not sure what information to provide here...
+ pass
+
+ self.valsLock.release()
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents updates from being presented.
+ """
+
+ if not self._isPaused == isPause:
+ self._isPaused = isPause
+ if self._isTorConnected:
+ if isPause: self._haltTime = time.time()
+ else: self._haltTime = None
+
+ # Redraw now so we'll be displaying the state right when paused
+ # (otherwise the uptime might be off by a second, and change when
+ # the panel's redrawn for other reasons).
+ self.redraw(True)
+
+ 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)
+ self._cond.release()
+ else:
+ # Update the volatile attributes (cpu, memory, flags, etc) if we have
+ # 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, conn, eventType):
+ """
+ Updates static parameters on tor reload (sighup) events.
+
+ Arguments:
+ conn - tor controller
+ eventType - type of event detected
+ """
+
+ if eventType == torTools.State.INIT:
+ self._isTorConnected = True
+ if self._isPaused: self._haltTime = time.time()
+ else: self._haltTime = None
+
+ self._update(True)
+ self.redraw(True)
+ elif eventType == torTools.State.CLOSED:
+ self._isTorConnected = False
+ 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
+ self.vals["tor/version"] = conn.getInfo("version", "Unknown").split()[0]
+ self.vals["tor/versionStatus"] = conn.getInfo("status/version/current", "Unknown")
+ self.vals["tor/nickname"] = conn.getOption("Nickname", "")
+ self.vals["tor/orPort"] = conn.getOption("ORPort", "0")
+ self.vals["tor/dirPort"] = conn.getOption("DirPort", "0")
+ self.vals["tor/controlPort"] = conn.getOption("ControlPort", "")
+ self.vals["tor/isAuthPassword"] = conn.getOption("HashedControlPassword") != None
+ self.vals["tor/isAuthCookie"] = conn.getOption("CookieAuthentication") == "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")
+ if listenAddr:
+ if ":" in listenAddr:
+ # both ip and port overwritten
+ self.vals["tor/orListenAddr"] = listenAddr[:listenAddr.find(":")]
+ 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]
+
+ pid = conn.getMyPid()
+ self.vals["tor/pid"] = pid if pid else ""
+
+ 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"] = []
+ self.vals["tor/fdUsed"] = 0
+ self.vals["stat/%torCpu"] = "0"
+ 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.
+ if self.vals["tor/fdLimit"]:
+ 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."
+ log.log(self._config["log.fdUsageNinetyPercent"], msg)
+ elif fdPercent >= 60 and not self._isFdSixtyPercentWarned:
+ self._isFdSixtyPercentWarned = True
+ log.log(self._config["log.fdUsageSixtyPercent"], 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"
+ self.vals["stat/%mem"] = "0"
+ else:
+ cpuUsage, _, memUsage, memUsagePercent = resourceTracker.getResourceUsage()
+ self._lastResourceFetch = resourceTracker.getRunCount()
+ 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]
+ pythonCpuTime = armCpuDelta / armTimeDelta
+ 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/src/cli/logPanel.py b/src/cli/logPanel.py
new file mode 100644
index 0000000..86e680f
--- /dev/null
+++ b/src/cli/logPanel.py
@@ -0,0 +1,1100 @@
+"""
+Panel providing a chronological log of events its been configured to listen
+for. This provides prepopulation from the log file and supports filtering by
+regular expressions.
+"""
+
+import time
+import os
+import curses
+import threading
+
+from TorCtl import TorCtl
+
+from version import VERSION
+from util import conf, log, panel, sysTools, torTools, uiTools
+
+TOR_EVENT_TYPES = {
+ "d": "DEBUG", "a": "ADDRMAP", "k": "DESCCHANGED", "s": "STREAM",
+ "i": "INFO", "f": "AUTHDIR_NEWDESCS", "g": "GUARD", "r": "STREAM_BW",
+ "n": "NOTICE", "h": "BUILDTIMEOUT_SET", "l": "NEWCONSENSUS", "t": "STATUS_CLIENT",
+ "w": "WARN", "b": "BW", "m": "NEWDESC", "u": "STATUS_GENERAL",
+ "e": "ERR", "c": "CIRC", "p": "NS", "v": "STATUS_SERVER",
+ "j": "CLIENTS_SEEN", "q": "ORCONN"}
+
+EVENT_LISTING = """ d DEBUG a ADDRMAP k DESCCHANGED s STREAM
+ i INFO f AUTHDIR_NEWDESCS g GUARD r STREAM_BW
+ n NOTICE h BUILDTIMEOUT_SET l NEWCONSENSUS t STATUS_CLIENT
+ w WARN b BW m NEWDESC u STATUS_GENERAL
+ e ERR c CIRC p NS v STATUS_SERVER
+ j CLIENTS_SEEN q ORCONN
+ DINWE tor runlevel+ A All Events
+ 12345 arm runlevel+ X No Events
+ 67890 torctl runlevel+ U Unknown Events"""
+
+RUNLEVEL_EVENT_COLOR = {log.DEBUG: "magenta", log.INFO: "blue", log.NOTICE: "green",
+ log.WARN: "yellow", log.ERR: "red"}
+DAYBREAK_EVENT = "DAYBREAK" # special event for marking when the date changes
+TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
+
+ENTRY_INDENT = 2 # spaces an entry's message is indented after the first line
+DEFAULT_CONFIG = {"features.logFile": "",
+ "features.log.showDateDividers": True,
+ "features.log.showDuplicateEntries": False,
+ "features.log.entryDuration": 7,
+ "features.log.maxLinesPerEntry": 4,
+ "features.log.prepopulate": True,
+ "features.log.prepopulateReadLimit": 5000,
+ "features.log.maxRefreshRate": 300,
+ "cache.logPanel.size": 1000,
+ "log.logPanel.prepopulateSuccess": log.INFO,
+ "log.logPanel.prepopulateFailed": log.WARN,
+ "log.logPanel.logFileOpened": log.NOTICE,
+ "log.logPanel.logFileWriteFailed": log.ERR,
+ "log.logPanel.forceDoubleRedraw": log.DEBUG}
+
+DUPLICATE_MSG = " [%i duplicate%s hidden]"
+
+# The height of the drawn content is estimated based on the last time we redrew
+# the panel. It's chiefly used for scrolling and the bar indicating its
+# position. Letting the estimate be too inaccurate results in a display bug, so
+# redraws the display if it's off by this threshold.
+CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
+
+# static starting portion of common log entries, fetched from the config when
+# needed if None
+COMMON_LOG_MESSAGES = None
+
+# cached values and the arguments that generated it for the getDaybreaks and
+# getDuplicates functions
+CACHED_DAYBREAKS_ARGUMENTS = (None, None) # events, current day
+CACHED_DAYBREAKS_RESULT = None
+CACHED_DUPLICATES_ARGUMENTS = None # events
+CACHED_DUPLICATES_RESULT = None
+
+# duration we'll wait for the deduplication function before giving up (in ms)
+DEDUPLICATION_TIMEOUT = 100
+
+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)
+
+def expandEvents(eventAbbr):
+ """
+ Expands event abbreviations to their full names. Beside mappings provided in
+ TOR_EVENT_TYPES this recognizes the following special events and aliases:
+ U - UKNOWN events
+ A - all events
+ X - no events
+ DINWE - runlevel and higher
+ 12345 - arm runlevel and higher (ARM_DEBUG - ARM_ERR)
+ 67890 - torctl runlevel and higher (TORCTL_DEBUG - TORCTL_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.values()]
+ torctlRunlevels = ["TORCTL_" + runlevel for runlevel in log.Runlevel.values()]
+ expandedEvents = set(TOR_EVENT_TYPES.values() + armRunlevels + torctlRunlevels + ["UNKNOWN"])
+ break
+ elif flag == "X":
+ expandedEvents = set()
+ break
+ elif flag in "DINWE1234567890":
+ # all events for a runlevel and higher
+ if flag in "DINWE": typePrefix = ""
+ elif flag in "12345": typePrefix = "ARM_"
+ elif flag in "67890": typePrefix = "TORCTL_"
+
+ if flag in "D16": runlevelIndex = 0
+ elif flag in "I27": runlevelIndex = 1
+ elif flag in "N38": runlevelIndex = 2
+ elif flag in "W49": runlevelIndex = 3
+ elif flag in "E50": runlevelIndex = 4
+
+ runlevelSet = [typePrefix + runlevel for runlevel in log.Runlevel.values()[runlevelIndex:]]
+ expandedEvents = expandedEvents.union(set(runlevelSet))
+ elif flag == "U":
+ expandedEvents.add("UNKNOWN")
+ elif flag in TOR_EVENT_TYPES:
+ expandedEvents.add(TOR_EVENT_TYPES[flag])
+ else:
+ invalidFlags += flag
+
+ if invalidFlags: raise ValueError(invalidFlags)
+ else: return expandedEvents
+
+def getMissingEventTypes():
+ """
+ Provides the event types the current torctl connection supports but arm
+ 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")
+
+ if torEventTypes:
+ torEventTypes = torEventTypes.split(" ")
+ armEventTypes = TOR_EVENT_TYPES.values()
+ return [event for event in torEventTypes if not event in armEventTypes]
+ else: return None # GETINFO call failed
+
+def loadLogMessages():
+ """
+ Fetches a mapping of common log messages to their runlevels from the config.
+ """
+
+ global COMMON_LOG_MESSAGES
+ armConf = conf.getConfig("arm")
+
+ COMMON_LOG_MESSAGES = {}
+ for confKey in armConf.getKeys():
+ if confKey.startswith("msg."):
+ eventType = confKey[4:].upper()
+ messages = armConf.get(confKey, [])
+ COMMON_LOG_MESSAGES[eventType] = messages
+
+def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = None):
+ """
+ Parses tor's log file for past events matching the given runlevels, providing
+ a list of log entries (ordered newest to oldest). Limiting the number of read
+ 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)
+ config - configuration parameters related to this panel, uses defaults
+ if left as None
+ """
+
+ startTime = time.time()
+ if not runlevels: return []
+
+ if not config: config = DEFAULT_CONFIG
+
+ # checks tor's configuration for the log file's location (if any exists)
+ loggingTypes, loggingLocation = None, None
+ for loggingEntry in torTools.getConn().getOption("Log", [], True):
+ # 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.getConn().getPathPrefix() + loggingLocation
+
+ # if the runlevels argument is a superset of the log file then we can
+ # limit the read contents to the addLimit
+ runlevels = log.Runlevel.values()
+ loggingTypes = loggingTypes.upper()
+ if addLimit and (not readLimit or readLimit > addLimit):
+ if "-" in loggingTypes:
+ divIndex = loggingTypes.find("-")
+ sIndex = runlevels.index(loggingTypes[:divIndex])
+ eIndex = runlevels.index(loggingTypes[divIndex+1:])
+ logFileRunlevels = runlevels[sIndex:eIndex+1]
+ 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:
+ if readLimit:
+ lines = sysTools.call("tail -n %i %s" % (readLimit, loggingLocation))
+ if not lines: raise IOError()
+ else:
+ logFile = open(loggingLocation, "r")
+ lines = logFile.readlines()
+ logFile.close()
+ except IOError:
+ msg = "Unable to read tor's log file: %s" % loggingLocation
+ log.log(config["log.logPanel.prepopulateFailed"], msg)
+
+ 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()
+ 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(".")]
+
+ # overwrites missing time parameters with the local time (ignoring wday
+ # and yday since they aren't used)
+ eventTimeComp = list(time.strptime(timestamp, "%b %d %H:%M:%S"))
+ eventTimeComp[0] = currentLocalTime.tm_year
+ 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]
+ msg = "Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(loggedEvents), loggingLocation, readLimit, time.time() - startTime)
+ log.log(config["log.logPanel.prepopulateSuccess"], msg)
+ return loggedEvents
+
+def getDaybreaks(events, ignoreTimeForCache = False):
+ """
+ Provides the input events back with special 'DAYBREAK_EVENT' markers inserted
+ 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):
+ """
+ Deduplicates a list of log entries, providing back a tuple listing with the
+ 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):
+ """
+ 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
+ elif event.type in COMMON_LOG_MESSAGES:
+ for commonMsg in COMMON_LOG_MESSAGES[event.type]:
+ # if it starts with an asterisk then check the whole message rather
+ # than just the start
+ if commonMsg[0] == "*":
+ 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
+
+class LogEntry():
+ """
+ Individual log file entry, having the following attributes:
+ timestamp - unix timestamp for when the event occurred
+ eventType - event type that occurred ("INFO", "BW", "ARM_WARN", etc)
+ 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 TorEventObserver(TorCtl.PostEventListener):
+ """
+ Listens for all types of events provided by TorCtl, providing an LogEntry
+ instance to the given callback function.
+ """
+
+ def __init__(self, callback):
+ """
+ Tor event listener with the purpose of translating events to nicely
+ formatted calls of a callback function.
+
+ Arguments:
+ callback - function accepting a LogEntry, called when an event of these
+ types occur
+ """
+
+ TorCtl.PostEventListener.__init__(self)
+ self.callback = callback
+
+ def circ_status_event(self, event):
+ msg = "ID: %-3s STATUS: %-10s PATH: %s" % (event.circ_id, event.status, ", ".join(event.path))
+ if event.purpose: msg += " PURPOSE: %s" % event.purpose
+ if event.reason: msg += " REASON: %s" % event.reason
+ if event.remote_reason: msg += " REMOTE_REASON: %s" % event.remote_reason
+ self._notify(event, msg, "yellow")
+
+ def buildtimeout_set_event(self, event):
+ self._notify(event, "SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (event.set_type, event.total_times, event.timeout_ms, event.xm, event.alpha, event.cutoff_quantile))
+
+ def stream_status_event(self, event):
+ self._notify(event, "ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose))
+
+ def or_conn_status_event(self, event):
+ msg = "STATUS: %-10s ENDPOINT: %-20s" % (event.status, event.endpoint)
+ if event.age: msg += " AGE: %-3s" % event.age
+ if event.read_bytes: msg += " READ: %-4i" % event.read_bytes
+ if event.wrote_bytes: msg += " WRITTEN: %-4i" % event.wrote_bytes
+ if event.reason: msg += " REASON: %-6s" % event.reason
+ if event.ncircs: msg += " NCIRCS: %i" % event.ncircs
+ self._notify(event, msg)
+
+ def stream_bw_event(self, event):
+ self._notify(event, "ID: %s READ: %s WRITTEN: %s" % (event.strm_id, event.bytes_read, event.bytes_written))
+
+ def bandwidth_event(self, event):
+ self._notify(event, "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
+
+ def msg_event(self, event):
+ self._notify(event, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
+
+ def new_desc_event(self, event):
+ idlistStr = [str(item) for item in event.idlist]
+ self._notify(event, ", ".join(idlistStr))
+
+ def address_mapped_event(self, event):
+ self._notify(event, "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr))
+
+ def ns_event(self, event):
+ # NetworkStatus params: nickname, idhash, orhash, ip, orport (int),
+ # dirport (int), flags, idhex, bandwidth, updated (datetime)
+ msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
+ self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "blue")
+
+ def new_consensus_event(self, event):
+ msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
+ self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
+
+ def unknown_event(self, event):
+ msg = "(%s) %s" % (event.event_name, event.event_string)
+ self.callback(LogEntry(event.arrived_at, "UNKNOWN", msg, "red"))
+
+ def _notify(self, event, msg, color="white"):
+ self.callback(LogEntry(event.arrived_at, event.event_name, msg, color))
+
+class LogPanel(panel.Panel, threading.Thread):
+ """
+ Listens for and displays tor, arm, and torctl events. This can prepopulate
+ from tor's log file if it exists.
+ """
+
+ def __init__(self, stdscr, loggedEvents, config=None):
+ panel.Panel.__init__(self, stdscr, "log", 0)
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+
+ self._config = dict(DEFAULT_CONFIG)
+
+ if config:
+ config.update(self._config, {
+ "features.log.maxLinesPerEntry": 1,
+ "features.log.prepopulateReadLimit": 0,
+ "features.log.maxRefreshRate": 10,
+ "cache.logPanel.size": 1000})
+
+ # collapses duplicate log entries if false, showing only the most recent
+ self.showDuplicates = self._config["features.log.showDuplicateEntries"]
+
+ self.msgLog = [] # log entries, sorted by the timestamp
+ self.loggedEvents = loggedEvents # events we're listening to
+ 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._isPaused = False
+ self._pauseBuffer = [] # location where messages are buffered if paused
+
+ 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, _pauseBuffer
+ 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)
+
+ # fetches past tor events from log file, if available
+ torEventBacklog = []
+ if self._config["features.log.prepopulate"]:
+ setRunlevels = list(set.intersection(set(self.loggedEvents), set(log.Runlevel.values())))
+ readLimit = self._config["features.log.prepopulateReadLimit"]
+ addLimit = self._config["cache.logPanel.size"]
+ torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config)
+
+ # adds arm listener and fetches past events
+ log.LOG_LOCK.acquire()
+ try:
+ armRunlevels = [log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR]
+ log.addListeners(armRunlevels, self._registerArmEvent)
+
+ # gets the set of arm events we're logging
+ setRunlevels = []
+ for i in range(len(armRunlevels)):
+ if "ARM_" + log.Runlevel.values()[i] in self.loggedEvents:
+ setRunlevels.append(armRunlevels[i])
+
+ armEventBacklog = []
+ for level, msg, eventTime in log._getEntries(setRunlevels):
+ armEventEntry = LogEntry(eventTime, "ARM_" + level, msg, RUNLEVEL_EVENT_COLOR[level])
+ armEventBacklog.insert(0, armEventEntry)
+
+ # joins armEventBacklog and torEventBacklog chronologically into msgLog
+ while armEventBacklog or torEventBacklog:
+ if not armEventBacklog:
+ self.msgLog.append(torEventBacklog.pop(0))
+ elif not torEventBacklog:
+ self.msgLog.append(armEventBacklog.pop(0))
+ elif armEventBacklog[0].timestamp < torEventBacklog[0].timestamp:
+ self.msgLog.append(torEventBacklog.pop(0))
+ else:
+ self.msgLog.append(armEventBacklog.pop(0))
+ finally:
+ log.LOG_LOCK.release()
+
+ # crops events that are either too old, or more numerous than the caching size
+ self._trimEvents(self.msgLog)
+
+ # leaving lastContentHeight as being too low causes initialization problems
+ self.lastContentHeight = len(self.msgLog)
+
+ # adds listeners for tor and torctl events
+ conn = torTools.getConn()
+ conn.addEventListener(TorEventObserver(self.registerEvent))
+ conn.addTorCtlListener(self._registerTorCtlEvent)
+
+ # opens log file if we'll be saving entries
+ if self._config["features.logFile"]:
+ logPath = self._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.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath))
+ except (IOError, OSError), exc:
+ log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
+ self.logFile = None
+
+ 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:
+ self.logFile.write(event.getDisplayMessage(True) + "\n")
+ self.logFile.flush()
+ except IOError, exc:
+ log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
+ self.logFile = None
+
+ if self._isPaused:
+ self.valsLock.acquire()
+ self._pauseBuffer.insert(0, event)
+ self._trimEvents(self._pauseBuffer)
+ self.valsLock.release()
+ else:
+ 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 _registerArmEvent(self, level, msg, eventTime):
+ eventColor = RUNLEVEL_EVENT_COLOR[level]
+ self.registerEvent(LogEntry(eventTime, "ARM_%s" % level, msg, eventColor))
+
+ def _registerTorCtlEvent(self, level, msg):
+ eventColor = RUNLEVEL_EVENT_COLOR[level]
+ self.registerEvent(LogEntry(time.time(), "TORCTL_%s" % level, msg, eventColor))
+
+ 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()
+ self.loggedEvents = eventTypes
+ self.redraw(True)
+ self.valsLock.release()
+
+ 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 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
+ """
+
+ # make dir if the path doesn't already exist
+ baseDir = os.path.dirname(path)
+ if not os.path.exists(baseDir): os.makedirs(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):
+ 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
+ self.redraw(True)
+ self.valsLock.release()
+ elif key in (ord('u'), ord('U')):
+ self.valsLock.acquire()
+ self.showDuplicates = not self.showDuplicates
+ self.redraw(True)
+ self.valsLock.release()
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents message log from being updated with new events.
+ """
+
+ if isPause == self._isPaused: return
+
+ self._isPaused = isPause
+ if self._isPaused: self._pauseBuffer = []
+ else:
+ self.valsLock.acquire()
+ self.msgLog = (self._pauseBuffer + self.msgLog)[:self._config["cache.logPanel.size"]]
+ self.redraw(True)
+ self.valsLock.release()
+
+ 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.
+ """
+
+ self.valsLock.acquire()
+ self._lastLoggedEvents, self._lastUpdate = list(self.msgLog), time.time()
+
+ # draws the top label
+ 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 self._config["features.log.showDateDividers"]
+ eventLog = getDaybreaks(self.msgLog, self._isPaused) if isDatesShown else list(self.msgLog)
+ if not self.showDuplicates:
+ deduplicatedLog = getDuplicates(eventLog)
+
+ if deduplicatedLog == None:
+ msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive."
+ log.log(log.WARN, msg)
+ self.showDuplicates = True
+ 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
+ if seenFirstDateDivider:
+ if lineCount >= 1 and lineCount < height and showDaybreaks:
+ self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr)
+ self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
+ self.addch(lineCount, width, 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) - 2
+ 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 = self._config["features.log.maxLinesPerEntry"]
+ while displayQueue:
+ msg, format, includeBreak = displayQueue.pop(0)
+ drawLine = lineCount + lineOffset
+ if lineOffset == maxEntriesPerLine: break
+
+ maxMsgSize = width - cursorLoc
+ if len(msg) > maxMsgSize:
+ # message is too long - break it up
+ if lineOffset == maxEntriesPerLine - 1:
+ msg = uiTools.cropStr(msg, maxMsgSize)
+ 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, 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 - 1, dividerAttr)
+ self.addch(lineCount, width, 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:
+ forceRedrawReason = "scrolled off the bottom of the page"
+ elif not isScrollBarVisible and newContentHeight > height - 1:
+ forceRedrawReason = "scroll bar wasn't previously visible"
+ elif isScrollBarVisible and newContentHeight <= height - 1:
+ forceRedrawReason = "scroll bar shouldn't be visible"
+ else: forceRedraw = False
+
+ self.lastContentHeight = newContentHeight
+ if forceRedraw:
+ forceRedrawReason = "redrawing the log panel with the corrected content height (%s)" % forceRedrawReason
+ log.log(self._config["log.logPanel.forceDoubleRedraw"], forceRedrawReason)
+ 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 = self._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)
+ self._cond.release()
+ else:
+ lastDay = currentDay
+ self.redraw(True)
+
+ def stop(self):
+ """
+ Halts further resolutions and terminates the thread.
+ """
+
+ self._cond.acquire()
+ self._halt = True
+ self._cond.notifyAll()
+ self._cond.release()
+
+ 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()
+ currentPattern = self.regexFilter.pattern if self.regexFilter else None
+ isUnchanged = self._titleArgs[0] == self.loggedEvents
+ isUnchanged &= self._titleArgs[1] == currentPattern
+ isUnchanged &= self._titleArgs[2] == width
+ if isUnchanged:
+ self.valsLock.release()
+ return self._titleCache
+
+ eventsList = list(self.loggedEvents)
+ if not eventsList:
+ if not currentPattern:
+ panelLabel = "Events:"
+ else:
+ labelPattern = uiTools.cropStr(currentPattern, width - 18)
+ panelLabel = "Events (filter: %s):" % labelPattern
+ else:
+ # does the following with all runlevel types (tor, arm, and torctl):
+ # - pulls to the start of the list
+ # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN")
+ # - condense further if there's identical runlevel ranges for multiple
+ # 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 = log.Runlevel.values()
+ reversedRunlevels.reverse()
+ for prefix in ("TORCTL_", "ARM_", ""):
+ # blank ending runlevel forces the break condition to be reached at the end
+ for runlevel in reversedRunlevels + [""]:
+ eventType = prefix + runlevel
+ if runlevel and eventType in eventsList:
+ # runlevel event found, move to the tmp list
+ eventsList.remove(eventType)
+ tmpRunlevels.append(runlevel)
+ elif tmpRunlevels:
+ # adds all tmp list entries to the start of eventsList
+ if len(tmpRunlevels) >= 3:
+ # save condense sequential runlevels to be added later
+ runlevelRanges.append((prefix, tmpRunlevels[-1], tmpRunlevels[0]))
+ else:
+ # 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 = self._config["cache.logPanel.size"]
+ if len(eventListing) > cacheSize: del eventListing[cacheSize:]
+
+ logTTL = self._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/src/cli/torrcPanel.py b/src/cli/torrcPanel.py
new file mode 100644
index 0000000..b7cad86
--- /dev/null
+++ b/src/cli/torrcPanel.py
@@ -0,0 +1,221 @@
+"""
+Panel displaying the torrc or armrc with the validation done against it.
+"""
+
+import math
+import curses
+import threading
+
+from util import conf, enum, panel, torConfig, uiTools
+
+DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
+ "features.config.file.maxLinesPerEntry": 8}
+
+# TODO: The armrc use case is incomplete. There should be equivilant reloading
+# and validation capabilities to the torrc.
+Config = enum.Enum("TORRC", "ARMRC") # configuration file types that can be displayed
+
+class TorrcPanel(panel.Panel):
+ """
+ Renders the current torrc or armrc with syntax highlighting in a scrollable
+ area.
+ """
+
+ def __init__(self, stdscr, configType, config=None):
+ panel.Panel.__init__(self, stdscr, "configFile", 0)
+
+ self._config = dict(DEFAULT_CONFIG)
+ if config:
+ config.update(self._config, {"features.config.file.maxLinesPerEntry": 1})
+
+ self.valsLock = threading.RLock()
+ self.configType = configType
+ self.scroll = 0
+ self.showLabel = True # shows top label (hides otherwise)
+ self.showLineNum = True # shows left aligned line numbers
+ self.stripComments = False # drops comments and extra whitespace
+
+ # height of the content when last rendered (the cached value is invalid if
+ # _lastContentHeightArgs is None or differs from the current dimensions)
+ self._lastContentHeight = 1
+ self._lastContentHeightArgs = None
+
+ def handleKey(self, key):
+ self.valsLock.acquire()
+ if uiTools.isScrollKey(key):
+ pageHeight = self.getPreferredSize()[0] - 1
+ newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
+
+ if self.scroll != newScroll:
+ self.scroll = newScroll
+ self.redraw(True)
+ elif key == ord('n') or key == ord('N'):
+ self.showLineNum = not self.showLineNum
+ self._lastContentHeightArgs = None
+ self.redraw(True)
+ elif key == ord('s') or key == ord('S'):
+ self.stripComments = not self.stripComments
+ self._lastContentHeightArgs = None
+ self.redraw(True)
+
+ self.valsLock.release()
+
+ def draw(self, 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.getConfig("arm")
+ confLocation = loadedArmrc.path
+ renderedContents = list(loadedArmrc.rawContents)
+
+ # offset to make room for the line numbers
+ lineNumOffset = 0
+ if self.showLineNum:
+ if len(renderedContents) == 0: lineNumOffset = 2
+ else: lineNumOffset = int(math.log10(len(renderedContents))) + 2
+
+ # draws left-hand scroll bar if content's longer than the height
+ scrollOffset = 0
+ if self._config["features.config.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.showLabel:
+ 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(" ")
+ if isMultiline:
+ # part of a multiline entry started on a previous line so everything
+ # is part of the argument
+ lineComp["argument"][0] = lineText
+ elif optionIndex == -1:
+ # no argument provided
+ lineComp["option"][0] = lineText
+ else:
+ optionText = strippedLine[:optionIndex]
+ 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")
+ elif lineIssue == torConfig.ValidationError.MISMATCH:
+ lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red")
+ lineComp["correction"][0] = " (%s)" % lineIssueMsg
+ else:
+ # For some types of configs the correction field is simply used to
+ # provide extra data (for instance, the type for tor state fields).
+ lineComp["correction"][0] = " (%s)" % lineIssueMsg
+ lineComp["correction"][1] = curses.A_BOLD | uiTools.getColor("magenta")
+
+ # draws the line number
+ if self.showLineNum and displayLine < height and displayLine >= 1:
+ lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
+ self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
+
+ # draws the rest of the components with line wrap
+ cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
+ maxLinesPerEntry = self._config["features.config.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
+ if lineOffset == maxLinesPerEntry - 1:
+ msg = uiTools.cropStr(msg, maxMsgSize)
+ else:
+ 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/src/interface/__init__.py b/src/interface/__init__.py
deleted file mode 100644
index 0f11fc1..0000000
--- a/src/interface/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Panels, popups, and handlers comprising the arm user interface.
-"""
-
-__all__ = ["configPanel", "connPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
-
diff --git a/src/interface/configPanel.py b/src/interface/configPanel.py
deleted file mode 100644
index fd6fb54..0000000
--- a/src/interface/configPanel.py
+++ /dev/null
@@ -1,364 +0,0 @@
-"""
-Panel presenting the configuration state for tor or arm. Options can be edited
-and the resulting configuration files saved.
-"""
-
-import curses
-import threading
-
-from util import conf, enum, panel, torTools, torConfig, uiTools
-
-DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
- "features.config.state.showPrivateOptions": False,
- "features.config.state.showVirtualOptions": False,
- "features.config.state.colWidth.option": 25,
- "features.config.state.colWidth.value": 15}
-
-# TODO: The arm use cases are incomplete since they currently can't be
-# modified, have their descriptions fetched, or even get a complete listing
-# of what's available.
-State = enum.Enum("TOR", "ARM") # state to be presented
-
-# mappings of option categories to the color for their entries
-CATEGORY_COLOR = {torConfig.Category.GENERAL: "green",
- torConfig.Category.CLIENT: "blue",
- torConfig.Category.RELAY: "yellow",
- torConfig.Category.DIRECTORY: "magenta",
- torConfig.Category.AUTHORITY: "red",
- torConfig.Category.HIDDEN_SERVICE: "cyan",
- torConfig.Category.TESTING: "white",
- torConfig.Category.UNKNOWN: "white"}
-
-# attributes of a ConfigEntry
-Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE",
- "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT")
-DEFAULT_SORT_ORDER = (Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT)
-FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
- Field.OPTION: ("Option Name", "blue"),
- Field.VALUE: ("Value", "cyan"),
- Field.TYPE: ("Arg Type", "green"),
- Field.ARG_USAGE: ("Arg Usage", "yellow"),
- Field.SUMMARY: ("Summary", "green"),
- Field.DESCRIPTION: ("Description", "white"),
- Field.MAN_ENTRY: ("Man Page Entry", "blue"),
- Field.IS_DEFAULT: ("Is Default", "magenta")}
-
-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
- self.fields[Field.ARG_USAGE] = manEntry.argUsage
- self.fields[Field.DESCRIPTION] = manEntry.description
- else:
- self.fields[Field.MAN_ENTRY] = 99999 # sorts non-man entries last
- 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)
- valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth)
- summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None)
- lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth)
- self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
- self.labelCacheArgs = argSet
-
- return self.labelCache
-
- 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"):
- confValue = "False" if confValue == "0" else "True"
- elif self.get(Field.TYPE) == "DataSize" and confValue.isdigit():
- confValue = uiTools.getSizeLabel(int(confValue))
- elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit():
- confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
-
- return confValue
-
-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, config=None):
- panel.Panel.__init__(self, stdscr, "configState", 0)
-
- self.sortOrdering = DEFAULT_SORT_ORDER
- self._config = dict(DEFAULT_CONFIG)
- if config:
- config.update(self._config, {
- "features.config.selectionDetails.height": 0,
- "features.config.state.colWidth.option": 5,
- "features.config.state.colWidth.value": 5})
-
- sortFields = Field.values()
- customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields))
-
- if customOrdering:
- self.sortOrdering = [sortFields[i] for i in customOrdering]
-
- self.configType = configType
- self.confContents = []
- self.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
-
- if self.configType == State.TOR:
- conn = torTools.getConn()
- customOptions = torConfig.getCustomOptions()
- configOptionLines = conn.getInfo("config/names", "").strip().split("\n")
-
- for line in configOptionLines:
- # lines are of the form "<option> <type>", like:
- # UseEntryGuards Boolean
- confOption, confType = line.strip().split(" ", 1)
-
- # skips private and virtual entries if not configured to show them
- if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
- continue
- elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual":
- continue
-
- self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions))
- elif self.configType == State.ARM:
- # loaded via the conf utility
- armConf = conf.getConfig("arm")
- for key in armConf.getKeys():
- 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 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: self.sortOrdering = ordering
- self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
- self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
- self.valsLock.release()
-
- def handleKey(self, key):
- self.valsLock.acquire()
- if uiTools.isScrollKey(key):
- pageHeight = self.getPreferredSize()[0] - 1
- detailPanelHeight = self._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 key == ord('a') or key == ord('A'):
- self.showAll = not self.showAll
- self.redraw(True)
- self.valsLock.release()
-
- def draw(self, width, height):
- self.valsLock.acquire()
-
- # draws the top label
- 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"
-
- # panel with details for the current selection
- detailPanelHeight = self._config["features.config.selectionDetails.height"]
- isScrollbarVisible = False
- if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
- # no detail panel
- detailPanelHeight = 0
- scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1)
- cursorSelection = self.getSelection()
- isScrollbarVisible = len(self._getConfigOptions()) > height - 1
- else:
- # Shrink detail panel if there isn't sufficient room for the whole
- # thing. The extra line is for the bottom border.
- detailPanelHeight = min(height - 1, detailPanelHeight + 1)
- scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight)
- cursorSelection = self.getSelection()
- isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1
-
- self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
-
- 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 = self._config["features.config.state.colWidth.option"]
- valueWidth = self._config["features.config.state.colWidth.value"]
- descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
-
- 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:
- valueAttr = []
- valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
- 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 - 2, 4, 4, uiTools.Ending.HYPHEN, True)
- descriptionContent = remainder.strip() + descriptionContent
- else:
- # this is the last line, end it with an ellipse
- msg = uiTools.cropStr(lineContent, width - 2, 4, 4)
-
- self.addstr(3 + i, 2, msg, selectionFormat)
-
diff --git a/src/interface/connections/__init__.py b/src/interface/connections/__init__.py
deleted file mode 100644
index 7a32d77..0000000
--- a/src/interface/connections/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Panels, popups, and handlers comprising the arm user interface.
-"""
-
-__all__ = ["circEntry", "connEntry", "connPanel", "entries"]
-
diff --git a/src/interface/connections/circEntry.py b/src/interface/connections/circEntry.py
deleted file mode 100644
index 80c3f81..0000000
--- a/src/interface/connections/circEntry.py
+++ /dev/null
@@ -1,216 +0,0 @@
-"""
-Connection panel entries for client circuits. This includes a header entry
-followed by an entry for each hop in the circuit. For instance:
-
-89.188.20.246:42667 --> 217.172.182.26 (de) General / Built 8.6m (CIRCUIT)
-| 85.8.28.4 (se) 98FBC3B2B93897A78CDD797EF549E6B62C9A8523 1 / Guard
-| 91.121.204.76 (fr) 546387D93F8D40CFF8842BB9D3A8EC477CEDA984 2 / Middle
-+- 217.172.182.26 (de) 5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86 3 / Exit
-"""
-
-import curses
-
-from interface.connections import entries, connEntry
-from util import torTools, uiTools
-
-# cached fingerprint -> (IP Address, ORPort) results
-RELAY_INFO = {}
-
-def getRelayInfo(fingerprint):
- """
- Provides the (IP Address, ORPort) tuple for the given relay. If the lookup
- fails then this returns ("192.168.0.1", "0").
-
- Arguments:
- fingerprint - relay to look up
- """
-
- if not fingerprint in RELAY_INFO:
- conn = torTools.getConn()
- failureResult = ("192.168.0.1", "0")
-
- nsEntry = conn.getConsensusEntry(fingerprint)
- if not nsEntry: return failureResult
-
- nsLineComp = nsEntry.split("\n")[0].split(" ")
- if len(nsLineComp) < 8: return failureResult
-
- RELAY_INFO[fingerprint] = (nsLineComp[6], nsLineComp[7])
-
- return RELAY_INFO[fingerprint]
-
-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]]
-
- if status == "BUILT" and not self.lines[0].isBuilt:
- exitIp, exitORPort = getRelayInfo(path[-1])
- self.lines[0].setExit(exitIp, exitORPort, path[-1])
-
- for i in range(len(path)):
- relayFingerprint = path[i]
- relayIp, relayOrPort = getRelayInfo(relayFingerprint)
-
- 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):
- """
- 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()])
- return [uiTools.DrawEntry("Building Circuit...", detailFormat)]
- else: return connEntry.ConnectionLine.getDetails(self, width)
-
-class CircLine(connEntry.ConnectionLine):
- """
- An individual hop in a circuit. This overwrites the displayed listing, but
- 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 getListingEntry(self, width, currentTime, listingType):
- """
- Provides the DrawEntry 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:
- # bracketing (3 characters)
- # placementLabel (14 characters)
- # gap between etc and placement label (5 characters)
-
- if self.isLast: bracket = (curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
- else: bracket = (curses.ACS_VLINE, ord(' '), ord(' '))
- baselineSpace = len(bracket) + 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)
- etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
- elif listingType == entries.ListingType.HOSTNAME:
- # min space for the hostname is 40 characters
- etc = self.getEtcContent(width - baselineSpace - 40, listingType)
- dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
- dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr())
- elif listingType == entries.ListingType.FINGERPRINT:
- # dst width is derived as:
- # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
- dst = "%-55s" % self.foreign.getFingerprint()
- etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
- else:
- # min space for the nickname is 56 characters
- etc = self.getEtcContent(width - baselineSpace - 56, listingType)
- dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
- dst = dstLayout % self.foreign.getNickname()
-
- drawEntry = uiTools.DrawEntry("%-14s" % self.placementLabel, lineFormat)
- drawEntry = uiTools.DrawEntry(" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat, drawEntry)
- drawEntry = uiTools.DrawEntry(dst + etc, lineFormat, drawEntry)
- drawEntry = uiTools.DrawEntry(bracket, curses.A_NORMAL, drawEntry, lockFormat = True)
- return drawEntry
-
diff --git a/src/interface/connections/connEntry.py b/src/interface/connections/connEntry.py
deleted file mode 100644
index e6c0d92..0000000
--- a/src/interface/connections/connEntry.py
+++ /dev/null
@@ -1,850 +0,0 @@
-"""
-Connection panel entries related to actual connections to or from the system
-(ie, results seen by netstat, lsof, etc).
-"""
-
-import time
-import curses
-
-from util import connections, enum, torTools, uiTools
-from interface.connections import entries
-
-# Connection Categories:
-# Inbound Relay connection, coming to us.
-# Outbound Relay connection, leaving us.
-# Exit Outbound relay connection leaving the Tor network.
-# Hidden Connections to a hidden service we're providing.
-# Socks Socks connections for applications using Tor.
-# Circuit Circuits our tor client has created.
-# Directory Fetching tor consensus information.
-# Control Tor controller (arm, vidalia, etc).
-
-Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
-CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue",
- Category.EXIT: "red", Category.HIDDEN: "magenta",
- Category.SOCKS: "yellow", Category.CIRCUIT: "cyan",
- Category.DIRECTORY: "magenta", Category.CONTROL: "red"}
-
-# static data for listing format
-# <src> --> <dst> <etc><padding>
-LABEL_FORMAT = "%s --> %s %s%s"
-LABEL_MIN_PADDING = 2 # min space between listing label and following data
-
-# sort value for scrubbed ip addresses
-SCRUBBED_IP_VAL = 255 ** 4
-
-CONFIG = {"features.connection.markInitialConnections": True,
- "features.connection.showExitPort": True,
- "features.connection.showColumn.fingerprint": True,
- "features.connection.showColumn.nickname": True,
- "features.connection.showColumn.destination": True,
- "features.connection.showColumn.expandedIp": True}
-
-def loadConfig(config):
- config.update(CONFIG)
-
-class Endpoint:
- """
- Collection of attributes associated with a connection endpoint. This is a
- 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 ORPort when searching for matching
- # fingerprints (otherwise the ORPort is assumed to be unknown)
- self.isORPort = False
-
- # 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)
- #except:
- # # either a ValueError or IOError depending on the source of the lookup failure
- # myHostname = None
- #
- #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()
- orPort = self.port if self.isORPort else None
- myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort)
-
- 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"
-
-class ConnectionEntry(entries.ConnectionPanelEntry):
- """
- Represents a connection being made to or from this system. These only
- 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
- return connLine.sortIpAddr
- elif attr == entries.SortAttr.PORT:
- return connLine.sortPort
- elif attr == entries.SortAttr.HOSTNAME:
- if connLine.isPrivate(): return ""
- return connLine.foreign.getHostname("")
- elif attr == entries.SortAttr.FINGERPRINT:
- return connLine.foreign.getFingerprint()
- elif attr == entries.SortAttr.NICKNAME:
- myNickname = connLine.foreign.getNickname()
- if myNickname == "UNKNOWN": return "z" * 20 # orders at the end
- else: return myNickname.lower()
- elif attr == entries.SortAttr.CATEGORY:
- return Category.indexOf(connLine.getType())
- elif attr == entries.SortAttr.UPTIME:
- return connLine.startTime
- elif attr == entries.SortAttr.COUNTRY:
- if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return ""
- else: return connLine.foreign.getLocale("")
- else:
- return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType)
-
-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")
-
- # 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")
- myDirPort = conn.getOption("DirPort")
- mySocksPort = conn.getOption("SocksPort", "9050")
- myCtlPort = conn.getOption("ControlPort")
- myHiddenServicePorts = conn.getHiddenServicePorts()
-
- # the ORListenAddress can overwrite the ORPort
- listenAddr = conn.getOption("ORListenAddress")
- if listenAddr and ":" in listenAddr:
- myOrPort = listenAddr[listenAddr.find(":") + 1:]
-
- if lPort in (myOrPort, myDirPort):
- self.baseType = Category.INBOUND
- self.local.isORPort = True
- elif lPort == mySocksPort:
- self.baseType = Category.SOCKS
- elif fPort in myHiddenServicePorts:
- self.baseType = Category.HIDDEN
- elif lPort == myCtlPort:
- self.baseType = Category.CONTROL
- else:
- self.baseType = Category.OUTBOUND
- self.foreign.isORPort = True
-
- 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 DrawEntry 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 = ""
-
- timeEntry = myListing.getNext()
- timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 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:
- # content - "<src> --> <dst> <etc> "
- # time - "<uptime>"
- # preType - " ("
- # category - "<type>"
- # postType - ") "
-
- lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType])
- timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5
-
- drawEntry = uiTools.DrawEntry(")" + " " * (9 - len(entryType)), lineFormat)
- drawEntry = uiTools.DrawEntry(entryType.upper(), lineFormat | curses.A_BOLD, drawEntry)
- drawEntry = uiTools.DrawEntry(" (", lineFormat, drawEntry)
- drawEntry = uiTools.DrawEntry(" " * timeWidth, lineFormat, drawEntry)
- drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), listingType), lineFormat, drawEntry)
- 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 [uiTools.DrawEntry(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.
- """
-
- # 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") == "1":
- allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
- return allMatches == []
- 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:
- if self.baseType == Category.OUTBOUND:
- # Currently the only non-static categories are OUTBOUND vs...
- # - 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[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[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
- if listingType == entries.ListingType.IP_ADDRESS:
- 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 + 10 and CONFIG["features.connection.showColumn.nickname"]:
- # show nickname (column width: remainder)
- nicknameSpace = width - usedSpace
- nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
- etc += ("%%-%is " % nicknameSpace) % nicknameLabel
- usedSpace += nicknameSpace + 2
- elif listingType == entries.ListingType.HOSTNAME:
- if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
- # 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
- nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
- etc += ("%%-%is " % nicknameSpace) % nicknameLabel
- usedSpace += (nicknameSpace + 2)
- elif listingType == entries.ListingType.FINGERPRINT:
- 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
- else:
- 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 + 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.
- #
- # 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:
- # 15 characters for source, and a min of 40 reserved for the destination
- # TODO: when actually functional the src and dst need to be swapped for
- # SOCKS and CONTROL connections
- 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():
- dst = ("%%-%is" % hostnameSpace) % "<scrubbed>"
- 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)
- elif listingType == entries.ListingType.FINGERPRINT:
- src = "localhost"
- 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:
- # base data requires 50 min characters
- src = self.local.getNickname()
- 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:]
-
- # The network status exit policy doesn't exist for older tor versions.
- # If unavailable we'll need the full exit policy which is on the
- # descriptor (if that's available).
-
- exitPolicy = "unknown"
- if len(nsLines) >= 4 and nsLines[3].startswith("p "):
- exitPolicy = nsLines[3][2:].replace(",", ", ")
- elif descEntry:
- # the descriptor has an individual line for each entry in the exit policy
- exitPolicyEntries = []
-
- for line in descEntry.split("\n"):
- if line.startswith("accept") or line.startswith("reject"):
- exitPolicyEntries.append(line.strip())
-
- exitPolicy = ", ".join(exitPolicyEntries)
-
- dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
- lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
- lines[3] = "published: %s %s" % (pubDate, pubTime)
- lines[4] = "flags: %s" % flags.replace(" ", ", ")
- lines[5] = "exit policy: %s" % exitPolicy
-
- 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/src/interface/connections/connPanel.py b/src/interface/connections/connPanel.py
deleted file mode 100644
index 79fe9df..0000000
--- a/src/interface/connections/connPanel.py
+++ /dev/null
@@ -1,398 +0,0 @@
-"""
-Listing of the currently established connections tor has made.
-"""
-
-import time
-import curses
-import threading
-
-from interface.connections import entries, connEntry, circEntry
-from util import connections, enum, panel, torTools, uiTools
-
-DEFAULT_CONFIG = {"features.connection.resolveApps": True,
- "features.connection.listingType": 0,
- "features.connection.refreshRate": 5}
-
-# height of the detail panel content, not counting top and bottom border
-DETAILS_HEIGHT = 7
-
-# listing types
-Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME)
-
-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, config=None):
- panel.Panel.__init__(self, stdscr, "conn", 0)
- threading.Thread.__init__(self)
- self.setDaemon(True)
-
- self._sortOrdering = DEFAULT_SORT_ORDER
- self._config = dict(DEFAULT_CONFIG)
-
- if config:
- config.update(self._config, {
- "features.connection.listingType": (0, len(Listing.values()) - 1),
- "features.connection.refreshRate": 1})
-
- sortFields = entries.SortAttr.values()
- customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
-
- if customOrdering:
- self._sortOrdering = [sortFields[i] for i in customOrdering]
-
- self._listingType = Listing.values()[self._config["features.connection.listingType"]]
- 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._isPaused = True # prevents updates if true
- self._pauseTime = None # time when the panel was paused
- self._halt = False # terminates thread if true
- self._cond = threading.Condition() # used for pausing the thread
- self.valsLock = threading.RLock()
-
- # 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
-
- self._update() # populates initial entries
- self._resolveApps(False) # resolves initial applications
-
- # 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
- torTools.getConn().addStatusListener(self.torStateListener)
-
- def torStateListener(self, conn, eventType):
- """
- Freezes the connection contents when Tor stops.
-
- Arguments:
- conn - tor controller
- eventType - type of event detected
- """
-
- self._isTorRunning = eventType == torTools.State.INIT
-
- if self._isPaused or not self._isTorRunning:
- if not self._pauseTime: self._pauseTime = time.time()
- else: self._pauseTime = None
-
- self.redraw(True)
-
- def setPaused(self, isPause):
- """
- If true, prevents the panel from updating.
- """
-
- if not self._isPaused == isPause:
- self._isPaused = isPause
-
- if isPause or not self._isTorRunning:
- if not self._pauseTime: self._pauseTime = time.time()
- else: self._pauseTime = None
-
- # redraws so the display reflects any changes between the last update
- # and being paused
- self.redraw(True)
-
- 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: self._sortOrdering = ordering
- self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
-
- self._entryLines = []
- for entry in self._entries:
- self._entryLines += entry.getLines()
- self.valsLock.release()
-
- def setListingType(self, listingType):
- """
- Sets the priority information presented by the panel.
-
- Arguments:
- listingType - Listing instance for the primary information to be shown
- """
-
- self.valsLock.acquire()
- self._listingType = listingType
-
- # if we're sorting by the listing then we need to resort
- if entries.SortAttr.LISTING in self._sortOrdering:
- self.setSortOrder()
-
- self.valsLock.release()
-
- def handleKey(self, key):
- self.valsLock.acquire()
-
- if uiTools.isScrollKey(key):
- pageHeight = self.getPreferredSize()[0] - 1
- if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
- isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
- if isChanged: self.redraw(True)
- elif uiTools.isSelectionKey(key):
- self._showDetails = not self._showDetails
- self.redraw(True)
-
- self.valsLock.release()
-
- def run(self):
- """
- Keeps connections listing updated, checking for new entries at a set rate.
- """
-
- lastDraw = time.time() - 1
- while not self._halt:
- currentTime = time.time()
-
- if self._isPaused or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]:
- self._cond.acquire()
- if not self._halt: self._cond.wait(0.2)
- self._cond.release()
- else:
- # 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) / self._config["features.connection.refreshRate"]
- lastDraw += self._config["features.connection.refreshRate"] * drawTicks
-
- def draw(self, width, height):
- self.valsLock.acquire()
-
- # 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._scroller.getCursorSelection(self._entryLines)
-
- # draws the detail panel if currently displaying it
- if self._showDetails:
- # 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)):
- drawEntries[i].render(self, 1 + i, 2)
-
- # title label with connection counts
- title = "Connection Details:" if self._showDetails else self._title
- self.addstr(0, 0, title, curses.A_STANDOUT)
-
- scrollOffset = 1
- if isScrollbarVisible:
- scrollOffset = 3
- self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset)
-
- currentTime = self._pauseTime if self._pauseTime else 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
-
- drawEntry = entryLine.getListingEntry(width - scrollOffset, currentTime, self._listingType)
- drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
- drawEntry.render(self, drawLine, scrollOffset, extraFormat)
- 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.
- """
-
- connResolver = connections.getResolver("tor")
- currentResolutionCount = connResolver.getResolutionCount()
- self.appResolveSinceUpdate = False
-
- if self._lastResourceFetch != currentResolutionCount:
- 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)
- del newCircuits[oldEntry.circuitID]
- elif isinstance(oldEntry, connEntry.ConnectionEntry):
- 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)
- if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT:
- newEntries.append(newConnEntry)
-
- 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 = connEntry.Category.values()
- typeCounts = dict((type, 0) for type in categoryTypes)
- for entry in newEntries:
- if isinstance(entry, connEntry.ConnectionEntry):
- 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 self._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.
- #
- # The application resolver might have given up querying (for instance, if
- # 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/src/interface/connections/entries.py b/src/interface/connections/entries.py
deleted file mode 100644
index 6b24412..0000000
--- a/src/interface/connections/entries.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""
-Interface for entries in the connection panel. These consist of two parts: the
-entry itself (ie, Tor connection, client circuit, etc) and the lines it
-consists of in the listing.
-"""
-
-from util import enum
-
-# attributes we can list entries by
-ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT",
- "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
-
-SORT_COLORS = {SortAttr.CATEGORY: "red", SortAttr.UPTIME: "yellow",
- SortAttr.LISTING: "green", SortAttr.IP_ADDRESS: "blue",
- SortAttr.PORT: "blue", SortAttr.HOSTNAME: "magenta",
- SortAttr.FINGERPRINT: "cyan", SortAttr.NICKNAME: "cyan",
- SortAttr.COUNTRY: "blue"}
-
-# maximum number of ports a system can have
-PORT_COUNT = 65536
-
-class ConnectionPanelEntry:
- """
- Common parent for connection panel entries. This consists of a list of lines
- 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
- sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT
- sortValue += self.getSortValue(SortAttr.PORT, listingType)
- return sortValue
- elif listingType == ListingType.HOSTNAME:
- return self.getSortValue(SortAttr.HOSTNAME, listingType)
- elif listingType == ListingType.FINGERPRINT:
- 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 getListingEntry(self, width, currentTime, listingType):
- """
- Provides a DrawEntry instance 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
- the current time (this may be ignored due to caching)
- 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 DrawEntry instances 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/src/interface/controller.py b/src/interface/controller.py
deleted file mode 100644
index 5060188..0000000
--- a/src/interface/controller.py
+++ /dev/null
@@ -1,1584 +0,0 @@
-#!/usr/bin/env python
-# controller.py -- arm interface (curses monitor for relay status)
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-"""
-Curses (terminal) interface for the arm relay status monitor.
-"""
-
-import os
-import re
-import math
-import time
-import curses
-import curses.textpad
-import socket
-from TorCtl import TorCtl
-
-import headerPanel
-import graphing.graphPanel
-import logPanel
-import configPanel
-import torrcPanel
-import descriptorPopup
-
-import interface.connections.connPanel
-import interface.connections.connEntry
-import interface.connections.entries
-from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools
-import graphing.bandwidthStats
-import graphing.connStats
-import graphing.resourceStats
-
-CONFIRM_QUIT = True
-REFRESH_RATE = 5 # seconds between redrawing screen
-MAX_REGEX_FILTERS = 5 # maximum number of previous regex filters that'll be remembered
-
-# enums for message in control label
-CTL_HELP, CTL_PAUSED = range(2)
-
-# panel order per page
-PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
-PAGES = [
- ["graph", "log"],
- ["conn"],
- ["config"],
- ["torrc"]]
-
-PAUSEABLE = ["header", "graph", "log", "conn"]
-
-CONFIG = {"log.torrc.readFailed": log.WARN,
- "features.graph.type": 1,
- "features.config.prepopulateEditValues": True,
- "queries.refreshRate.rate": 5,
- "log.torEventTypeUnrecognized": log.NOTICE,
- "features.graph.bw.prepopulate": True,
- "log.startTime": log.INFO,
- "log.refreshRate": log.DEBUG,
- "log.highCpuUsage": log.WARN,
- "log.configEntryUndefined": log.NOTICE,
- "log.torrc.validation.torStateDiffers": log.WARN,
- "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE}
-
-class ControlPanel(panel.Panel):
- """ Draws single line label for interface controls. """
-
- def __init__(self, stdscr, isBlindMode):
- panel.Panel.__init__(self, stdscr, "control", 0, 1)
- self.msgText = CTL_HELP # message text to be displyed
- self.msgAttr = curses.A_NORMAL # formatting attributes
- self.page = 1 # page number currently being displayed
- self.resolvingCounter = -1 # count of resolver when starting (-1 if we aren't working on a batch)
- self.isBlindMode = isBlindMode
-
- def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
- """
- Sets the message and display attributes. If msgType matches CTL_HELP or
- CTL_PAUSED then uses the default message for those statuses.
- """
-
- self.msgText = msgText
- self.msgAttr = msgAttr
-
- def draw(self, width, height):
- msgText = self.msgText
- msgAttr = self.msgAttr
- barTab = 2 # space between msgText and progress bar
- barWidthMax = 40 # max width to progress bar
- barWidth = -1 # space between "[ ]" in progress bar (not visible if -1)
- barProgress = 0 # cells to fill
-
- if msgText == CTL_HELP:
- msgAttr = curses.A_NORMAL
-
- if self.resolvingCounter != -1:
- if hostnames.isPaused() or not hostnames.isResolving():
- # done resolving dns batch
- self.resolvingCounter = -1
- curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate
- else:
- batchSize = hostnames.getRequestCount() - self.resolvingCounter
- entryCount = batchSize - hostnames.getPendingCount()
- if batchSize > 0: progress = 100 * entryCount / batchSize
- else: progress = 0
-
- additive = "or l " if self.page == 2 else ""
- batchSizeDigits = int(math.log10(batchSize)) + 1
- entryCountLabel = ("%%%ii" % batchSizeDigits) % entryCount
- #msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
- msgText = "Resolving hostnames (press esc %sto cancel) - %s / %i, %2i%%" % (additive, entryCountLabel, batchSize, progress)
-
- barWidth = min(barWidthMax, width - len(msgText) - 3 - barTab)
- barProgress = barWidth * entryCount / batchSize
-
- if self.resolvingCounter == -1:
- currentPage = self.page
- pageCount = len(PAGES)
-
- if self.isBlindMode:
- if currentPage >= 2: currentPage -= 1
- pageCount -= 1
-
- msgText = "page %i / %i - q: quit, p: pause, h: page help" % (currentPage, pageCount)
- elif msgText == CTL_PAUSED:
- msgText = "Paused"
- msgAttr = curses.A_STANDOUT
-
- self.addstr(0, 0, msgText, msgAttr)
- if barWidth > -1:
- xLoc = len(msgText) + barTab
- self.addstr(0, xLoc, "[", curses.A_BOLD)
- self.addstr(0, xLoc + 1, " " * barProgress, curses.A_STANDOUT | uiTools.getColor("red"))
- self.addstr(0, xLoc + barWidth + 1, "]", curses.A_BOLD)
-
-class Popup(panel.Panel):
- """
- Temporarily providing old panel methods until permanent workaround for popup
- can be derrived (this passive drawing method is horrible - I'll need to
- provide a version using the more active repaint design later in the
- revision).
- """
-
- def __init__(self, stdscr, height):
- panel.Panel.__init__(self, stdscr, "popup", 0, height)
-
- # The following methods are to emulate old panel functionality (this was the
- # only implementations to use these methods and will require a complete
- # rewrite when refactoring gets here)
- def clear(self):
- if self.win:
- self.isDisplaced = self.top > self.win.getparyx()[0]
- if not self.isDisplaced: self.win.erase()
-
- def refresh(self):
- if self.win and not self.isDisplaced: self.win.refresh()
-
- def recreate(self, stdscr, newWidth=-1, newTop=None):
- self.setParent(stdscr)
- self.setWidth(newWidth)
- if newTop != None: self.setTop(newTop)
-
- newHeight, newWidth = self.getPreferredSize()
- if newHeight > 0:
- self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
- elif self.win == None:
- # don't want to leave the window as none (in very edge cases could cause
- # problems) - rather, create a displaced instance
- self.win = self.parent.subwin(1, newWidth, 0, 0)
-
- self.maxY, self.maxX = self.win.getmaxyx()
-
-def addstr_wrap(panel, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
- """
- Writes text with word wrapping, returning the ending y/x coordinate.
- y: starting write line
- x: column offset from startX
- text / formatting: content to be written
- startX / endX: column bounds in which text may be written
- """
-
- # moved out of panel (trying not to polute new code!)
- # TODO: unpleaseantly complex usage - replace with something else when
- # rewriting confPanel and descriptorPopup (the only places this is used)
- if not text: return (y, x) # nothing to write
- if endX == -1: endX = panel.maxX # defaults to writing to end of panel
- if maxY == -1: maxY = panel.maxY + 1 # defaults to writing to bottom of panel
- lineWidth = endX - startX # room for text
- while True:
- if len(text) > lineWidth - x - 1:
- chunkSize = text.rfind(" ", 0, lineWidth - x)
- writeText = text[:chunkSize]
- text = text[chunkSize:].strip()
-
- panel.addstr(y, x + startX, writeText, formatting)
- y, x = y + 1, 0
- if y >= maxY: return (y, x)
- else:
- panel.addstr(y, x + startX, text, formatting)
- return (y, x + len(text))
-
-class sighupListener(TorCtl.PostEventListener):
- """
- Listens for reload signal (hup), which is produced by:
- pkill -sighup tor
- causing the torrc and internal state to be reset.
- """
-
- def __init__(self):
- TorCtl.PostEventListener.__init__(self)
- self.isReset = False
-
- def msg_event(self, event):
- self.isReset |= event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)")
-
-def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
- """
- Resets the isPaused state of panels. If overwrite is True then this pauses
- reguardless of the monitor is paused or not.
- """
-
- for key in PAUSEABLE: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S))
-
-def showMenu(stdscr, popup, title, options, initialSelection):
- """
- 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. If initialSelection is -1 then the first
- option is used and the carrot indicating past selection is ommitted.
- """
-
- selection = initialSelection if initialSelection != -1 else 0
-
- if popup.win:
- if not panel.CURSES_LOCK.acquire(False): return -1
- try:
- # TODO: should pause interface (to avoid event accumilation)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
-
- # uses smaller dimentions more fitting for small content
- popup.height = len(options) + 2
-
- newWidth = max([len(label) for label in options]) + 9
- popup.recreate(stdscr, newWidth)
-
- key = 0
- while not uiTools.isSelectionKey(key):
- popup.clear()
- 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 == initialSelection else " "
- popup.addstr(i + 1, 2, tab)
- popup.addstr(i + 1, 4, " %s " % label, format)
-
- popup.refresh()
- key = stdscr.getch()
- if key == curses.KEY_UP: selection = max(0, selection - 1)
- elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
- elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
-
- # reverts popup dimensions and conn panel label
- popup.height = 9
- popup.recreate(stdscr, 80)
-
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
-
- return selection
-
-def showSortDialog(stdscr, panels, isPaused, page, titleLabel, 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:
- stdscr, panels, isPaused, page - boiler plate arguments of the controller
- (should be refactored away when rewriting)
-
- titleLabel - title displayed for the popup window
- options - ordered listing of option labels
- oldSelection - current ordering
- optionColors - mappings of options to their color
-
- """
-
- panel.CURSES_LOCK.acquire()
- newSelections = [] # new ordering
-
- try:
- setPauseState(panels, isPaused, page, True)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
-
- popup = panels["popup"]
- cursorLoc = 0 # index of highlighted option
-
- # label for the inital ordering
- formattedPrevListing = []
- for sortType in oldSelection:
- colorStr = optionColors.get(sortType, "white")
- formattedPrevListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
- prevOrderingLabel = "<b>Current Order: %s</b>" % ", ".join(formattedPrevListing)
-
- selectionOptions = list(options)
- selectionOptions.append("Cancel")
-
- while len(newSelections) < len(oldSelection):
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, titleLabel, curses.A_STANDOUT)
- popup.addfstr(1, 2, prevOrderingLabel)
-
- # provides new ordering
- formattedNewListing = []
- for sortType in newSelections:
- colorStr = optionColors.get(sortType, "white")
- formattedNewListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
- newOrderingLabel = "<b>New Order: %s</b>" % ", ".join(formattedNewListing)
- popup.addfstr(2, 2, newOrderingLabel)
-
- # presents remaining options, each row having up to four options with
- # spacing of nineteen cells
- row, col = 4, 0
- for i in range(len(selectionOptions)):
- popup.addstr(row, col * 19 + 2, selectionOptions[i], curses.A_STANDOUT if cursorLoc == i else curses.A_NORMAL)
- col += 1
- if col == 4: row, col = row + 1, 0
-
- popup.refresh()
-
- key = stdscr.getch()
- if key == curses.KEY_LEFT: cursorLoc = max(0, cursorLoc - 1)
- elif key == curses.KEY_RIGHT: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 1)
- elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4)
- elif key == curses.KEY_DOWN: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 4)
- elif uiTools.isSelectionKey(key):
- # selected entry (the ord of '10' seems needed to pick up enter)
- selection = selectionOptions[cursorLoc]
- if selection == "Cancel": break
- else:
- newSelections.append(selection)
- selectionOptions.remove(selection)
- cursorLoc = min(cursorLoc, len(selectionOptions) - 1)
- elif key == 27: break # esc - cancel
-
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
-
- if len(newSelections) == len(oldSelection):
- return newSelections
- else: return None
-
-def setEventListening(selectedEvents, isBlindMode):
- # creates a local copy, note that a suspected python bug causes *very*
- # puzzling results otherwise when trying to discard entries (silently
- # returning out of this function!)
- events = set(selectedEvents)
- isLoggingUnknown = "UNKNOWN" in events
-
- # removes special types only used in arm (UNKNOWN, TORCTL, ARM_DEBUG, etc)
- toDiscard = []
- for eventType in events:
- if eventType not in logPanel.TOR_EVENT_TYPES.values(): toDiscard += [eventType]
-
- for eventType in list(toDiscard): events.discard(eventType)
-
- # adds events unrecognized by arm if we're listening to the 'UNKNOWN' type
- if isLoggingUnknown:
- events.update(set(logPanel.getMissingEventTypes()))
-
- setEvents = torTools.getConn().setControllerEvents(list(events))
-
- # temporary hack for providing user selected events minus those that failed
- # (wouldn't be a problem if I wasn't storing tor and non-tor events together...)
- returnVal = list(selectedEvents.difference(torTools.FAILED_EVENTS))
- returnVal.sort() # alphabetizes
- return returnVal
-
-def connResetListener(conn, eventType):
- """
- Pauses connection resolution when tor's shut down, and resumes if started
- again.
- """
-
- if connections.isResolverAlive("tor"):
- resolver = connections.getResolver("tor")
- resolver.setPaused(eventType == torTools.State.CLOSED)
-
-def selectiveRefresh(panels, page):
- """
- This forces a redraw of content on the currently active page (should be done
- after changing pages, popups, or anything else that overwrites panels).
- """
-
- for panelKey in PAGES[page]:
- panels[panelKey].redraw(True)
-
-def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
- """
- Starts arm interface reflecting information on provided control port.
-
- stdscr - curses window
- conn - active Tor control port connection
- loggedEvents - types of events to be logged (plus an optional "UNKNOWN" for
- otherwise unrecognized events)
- """
-
- # loads config for various interface components
- config = conf.getConfig("arm")
- config.update(CONFIG)
- graphing.graphPanel.loadConfig(config)
- interface.connections.connEntry.loadConfig(config)
-
- # 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 isBlindMode:
- torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections"
-
- # pauses/unpauses connection resolution according to if tor's connected or not
- torTools.getConn().addStatusListener(connResetListener)
-
- # TODO: incrementally drop this requirement until everything's using the singleton
- conn = torTools.getConn().getTorCtl()
-
- curses.halfdelay(REFRESH_RATE * 10) # uses getch call as timer for REFRESH_RATE seconds
- try: curses.use_default_colors() # allows things like semi-transparent backgrounds (call can fail with ERR)
- except curses.error: pass
-
- # attempts to make the cursor invisible (not supported in all terminals)
- try: curses.curs_set(0)
- except curses.error: pass
-
- # attempts to determine tor's current pid (left as None if unresolveable, logging an error later)
- torPid = torTools.getConn().getMyPid()
-
- #try:
- # confLocation = conn.get_info("config-file")["config-file"]
- # if confLocation[0] != "/":
- # # relative path - attempt to add process pwd
- # try:
- # results = sysTools.call("pwdx %s" % torPid)
- # if len(results) == 1 and len(results[0].split()) == 2: confLocation = "%s/%s" % (results[0].split()[1], confLocation)
- # except IOError: pass # pwdx call failed
- #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
- # confLocation = ""
-
- # loads the torrc and provides warnings in case of validation errors
- loadedTorrc = torConfig.getTorrc()
- loadedTorrc.getLock().acquire()
-
- try:
- loadedTorrc.load()
- except IOError, exc:
- msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
- log.log(CONFIG["log.torrc.readFailed"], msg)
-
- if loadedTorrc.isLoaded():
- corrections = loadedTorrc.getCorrections()
- duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], []
-
- for lineNum, issue, msg in corrections:
- if issue == torConfig.ValidationError.DUPLICATE:
- duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1))
- elif issue == torConfig.ValidationError.IS_DEFAULT:
- defaultOptions.append("%s (line %i)" % (msg, lineNum + 1))
- elif issue == torConfig.ValidationError.MISMATCH: mismatchLines.append(lineNum + 1)
- elif issue == torConfig.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.log(CONFIG["log.torrc.validation.unnecessaryTorrcEntries"], msg)
-
- if mismatchLines or missingOptions:
- msg = "The torrc differ 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.log(CONFIG["log.torrc.validation.torStateDiffers"], msg)
-
- loadedTorrc.getLock().release()
-
- # minor refinements for connection resolver
- if not isBlindMode:
- if torPid:
- # use the tor pid to help narrow connection results
- torCmdName = sysTools.getProcessName(torPid, "tor")
- resolver = connections.getResolver(torCmdName, torPid, "tor")
- else:
- resolver = connections.getResolver("tor")
-
- # hack to display a better (arm specific) notice if all resolvers fail
- connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
-
- panels = {
- "header": headerPanel.HeaderPanel(stdscr, startTime, config),
- "popup": Popup(stdscr, 9),
- "graph": graphing.graphPanel.GraphPanel(stdscr),
- "log": logPanel.LogPanel(stdscr, loggedEvents, config)}
-
- # TODO: later it would be good to set the right 'top' values during initialization,
- # but for now this is just necessary for the log panel (and a hack in the log...)
-
- # TODO: bug from not setting top is that the log panel might attempt to draw
- # before being positioned - the following is a quick hack til rewritten
- panels["log"].setPaused(True)
-
- panels["conn"] = interface.connections.connPanel.ConnectionPanel(stdscr, config)
-
- panels["control"] = ControlPanel(stdscr, isBlindMode)
- panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.State.TOR, config)
- panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.Config.TORRC, config)
-
- # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
- if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
-
- # statistical monitors for graph
- panels["graph"].addStats("bandwidth", graphing.bandwidthStats.BandwidthStats(config))
- panels["graph"].addStats("system resources", graphing.resourceStats.ResourceStats())
- if not isBlindMode: panels["graph"].addStats("connections", graphing.connStats.ConnStats())
-
- # sets graph based on config parameter
- graphType = CONFIG["features.graph.type"]
- if graphType == 0: panels["graph"].setStats(None)
- elif graphType == 1: panels["graph"].setStats("bandwidth")
- elif graphType == 2 and not isBlindMode: panels["graph"].setStats("connections")
- elif graphType == 3: panels["graph"].setStats("system resources")
-
- # listeners that update bandwidth and log panels with Tor status
- sighupTracker = sighupListener()
- #conn.add_event_listener(panels["log"])
- conn.add_event_listener(panels["graph"].stats["bandwidth"])
- conn.add_event_listener(panels["graph"].stats["system resources"])
- if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
- conn.add_event_listener(sighupTracker)
-
- # prepopulates bandwidth values from state file
- if CONFIG["features.graph.bw.prepopulate"]:
- isSuccessful = panels["graph"].stats["bandwidth"].prepopulateFromState()
- if isSuccessful: panels["graph"].updateInterval = 4
-
- # tells Tor to listen to the events we're interested
- loggedEvents = setEventListening(loggedEvents, isBlindMode)
- #panels["log"].loggedEvents = loggedEvents # strips any that couldn't be set
- panels["log"].setLoggedEvents(loggedEvents) # strips any that couldn't be set
-
- # directs logged TorCtl events to log panel
- #TorUtil.loglevel = "DEBUG"
- #TorUtil.logfile = panels["log"]
- #torTools.getConn().addTorCtlListener(panels["log"].tor_ctl_event)
-
- # provides a notice about any event types tor supports but arm doesn't
- missingEventTypes = logPanel.getMissingEventTypes()
- if missingEventTypes:
- pluralLabel = "s" if len(missingEventTypes) > 1 else ""
- log.log(CONFIG["log.torEventTypeUnrecognized"], "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)))
-
- # tells revised panels to run as daemons
- panels["header"].start()
- panels["log"].start()
- panels["conn"].start()
-
- # warns if tor isn't updating descriptors
- #try:
- # if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
- # warning = """Descriptors won't be updated (causing some connection information to be stale) unless:
- #a. 'FetchUselessDescriptors 1' is set in your torrc
- #b. the directory service is provided ('DirPort' defined)
- #c. or tor is used as a client"""
- # log.log(log.WARN, warning)
- #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
-
- isUnresponsive = False # true if it's been over ten seconds since the last BW event (probably due to Tor closing)
- isPaused = False # if true updates are frozen
- overrideKey = None # immediately runs with this input rather than waiting for the user if set
- page = 0
- regexFilters = [] # previously used log regex filters
- panels["popup"].redraw(True) # hack to make sure popup has a window instance (not entirely sure why...)
-
- # provides notice about any unused config keys
- for key in config.getUnusedKeys():
- log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
-
- lastPerformanceLog = 0 # ensures we don't do performance logging too frequently
- redrawStartTime = time.time()
-
- # TODO: popups need to force the panels it covers to redraw (or better, have
- # a global refresh function for after changing pages, popups, etc)
-
- initTime = time.time() - startTime
- log.log(CONFIG["log.startTime"], "arm started (initialization took %0.3f seconds)" % initTime)
-
- # attributes to give a WARN level event if arm's resource usage is too high
- isResourceWarningGiven = False
- lastResourceCheck = startTime
-
- lastSize = None
-
- # sets initial visiblity for the pages
- for i in range(len(PAGES)):
- isVisible = i == page
- for entry in PAGES[i]: panels[entry].setVisible(isVisible)
-
- # TODO: come up with a nice, clean method for other threads to immediately
- # terminate the draw loop and provide a stacktrace
- while True:
- # tried only refreshing when the screen was resized but it caused a
- # noticeable lag when resizing and didn't have an appreciable effect
- # on system usage
-
- panel.CURSES_LOCK.acquire()
- try:
- redrawStartTime = time.time()
-
- # if sighup received then reload related information
- if sighupTracker.isReset:
- #panels["header"]._updateParams(True)
-
- # other panels that use torrc data
- #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
- #panels["graph"].stats["bandwidth"].resetOptions()
-
- # if bandwidth graph is being shown then height might have changed
- if panels["graph"].currentDisplay == "bandwidth":
- panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getContentHeight())
-
- # TODO: should redraw the torrcPanel
- #panels["torrc"].loadConfig()
-
- # reload the torrc if it's previously been loaded
- if loadedTorrc.isLoaded():
- try:
- loadedTorrc.load()
- if page == 3: panels["torrc"].redraw(True)
- except IOError, exc:
- msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
- log.log(CONFIG["log.torrc.readFailed"], msg)
-
- sighupTracker.isReset = False
-
- # gives panels a chance to take advantage of the maximum bounds
- # originally this checked in the bounds changed but 'recreate' is a no-op
- # if panel properties are unchanged and checking every redraw is more
- # resilient in case of funky changes (such as resizing during popups)
-
- # hack to make sure header picks layout before using the dimensions below
- #panels["header"].getPreferredSize()
-
- startY = 0
- for panelKey in PAGE_S[:2]:
- #panels[panelKey].recreate(stdscr, -1, startY)
- panels[panelKey].setParent(stdscr)
- panels[panelKey].setWidth(-1)
- panels[panelKey].setTop(startY)
- startY += panels[panelKey].getHeight()
-
- panels["popup"].recreate(stdscr, 80, startY)
-
- for panelSet in PAGES:
- tmpStartY = startY
-
- for panelKey in panelSet:
- #panels[panelKey].recreate(stdscr, -1, tmpStartY)
- panels[panelKey].setParent(stdscr)
- panels[panelKey].setWidth(-1)
- panels[panelKey].setTop(tmpStartY)
- tmpStartY += panels[panelKey].getHeight()
-
- # provides a notice if there's been ten seconds since the last BW event
- lastHeartbeat = torTools.getConn().getHeartbeat()
- if torTools.getConn().isAlive() and "BW" in torTools.getConn().getControllerEvents() and lastHeartbeat != 0:
- if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
- isUnresponsive = True
- log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
- elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
- # really shouldn't happen (meant Tor froze for a bit)
- isUnresponsive = False
- log.log(log.NOTICE, "Relay resumed")
-
- # TODO: part two of hack to prevent premature drawing by log panel
- if page == 0 and not isPaused: panels["log"].setPaused(False)
-
- # I haven't the foggiest why, but doesn't work if redrawn out of order...
- for panelKey in (PAGE_S + PAGES[page]):
- # redrawing popup can result in display flicker when it should be hidden
- if panelKey != "popup":
- newSize = stdscr.getmaxyx()
- isResize = lastSize != newSize
- lastSize = newSize
-
- if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"):
- # revised panel (manages its own content refreshing)
- panels[panelKey].redraw(isResize)
- else:
- panels[panelKey].redraw(True)
-
- stdscr.refresh()
-
- currentTime = time.time()
- if currentTime - lastPerformanceLog >= CONFIG["queries.refreshRate.rate"]:
- cpuTotal = sum(os.times()[:3])
- pythonCpuAvg = cpuTotal / (currentTime - startTime)
- sysCallCpuAvg = sysTools.getSysCpuUsage()
- totalCpuAvg = pythonCpuAvg + sysCallCpuAvg
-
- if sysCallCpuAvg > 0.00001:
- log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%% (python), %0.3f%% (system calls), %0.3f%% (total)" % (currentTime - redrawStartTime, 100 * pythonCpuAvg, 100 * sysCallCpuAvg, 100 * totalCpuAvg))
- else:
- # with the proc enhancements the sysCallCpuAvg is usually zero
- log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%%" % (currentTime - redrawStartTime, 100 * totalCpuAvg))
-
- lastPerformanceLog = currentTime
-
- # once per minute check if the sustained cpu usage is above 5%, if so
- # then give a warning (and if able, some advice for lowering it)
- # TODO: disabling this for now (scrolling causes cpu spikes for quick
- # redraws, ie this is usually triggered by user input)
- if False and not isResourceWarningGiven and currentTime > (lastResourceCheck + 60):
- if totalCpuAvg >= 0.05:
- msg = "Arm's cpu usage is high (averaging %0.3f%%)." % (100 * totalCpuAvg)
-
- if not isBlindMode:
- msg += " You could lower it by dropping the connection data (running as \"arm -b\")."
-
- log.log(CONFIG["log.highCpuUsage"], msg)
- isResourceWarningGiven = True
-
- lastResourceCheck = currentTime
- finally:
- panel.CURSES_LOCK.release()
-
- # wait for user keyboard input until timeout (unless an override was set)
- if overrideKey:
- key = overrideKey
- overrideKey = None
- else:
- key = stdscr.getch()
-
- if key == ord('q') or key == ord('Q'):
- quitConfirmed = not CONFIRM_QUIT
-
- # provides prompt to confirm that arm should exit
- if CONFIRM_QUIT:
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("Are you sure (q again to confirm)?", curses.A_BOLD)
- panels["control"].redraw(True)
-
- curses.cbreak()
- confirmationKey = stdscr.getch()
- quitConfirmed = confirmationKey in (ord('q'), ord('Q'))
- curses.halfdelay(REFRESH_RATE * 10)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
-
- if quitConfirmed:
- # quits arm
- # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
- # this appears to be a python bug: http://bugs.python.org/issue3014
- # (haven't seen this is quite some time... mysteriously resolved?)
-
- torTools.NO_SPAWN = True # prevents further worker threads from being spawned
-
- # stops panel daemons
- panels["header"].stop()
- panels["conn"].stop()
- panels["log"].stop()
-
- panels["header"].join()
- panels["conn"].join()
- panels["log"].join()
-
- # joins on utility daemon threads - this might take a moment since
- # the internal threadpools being joined might be sleeping
- conn = torTools.getConn()
- myPid = conn.getMyPid()
-
- resourceTracker = sysTools.getResourceTracker(myPid) if (myPid and sysTools.isTrackerAlive(myPid)) else None
- resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
- if resourceTracker: resourceTracker.stop()
- if resolver: resolver.stop() # sets halt flag (returning immediately)
- hostnames.stop() # halts and joins on hostname worker thread pool
- if resourceTracker: resourceTracker.join()
- if resolver: resolver.join() # joins on halted resolver
-
- conn.close() # joins on TorCtl event thread
- break
- elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
- # switch page
- if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
- else: page = (page + 1) % len(PAGES)
-
- # skip connections listing if it's disabled
- if page == 1 and isBlindMode:
- if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
- else: page = (page + 1) % len(PAGES)
-
- # pauses panels that aren't visible to prevent events from accumilating
- # (otherwise they'll wait on the curses lock which might get demanding)
- setPauseState(panels, isPaused, page)
-
- # prevents panels on other pages from redrawing
- for i in range(len(PAGES)):
- isVisible = i == page
- for entry in PAGES[i]: panels[entry].setVisible(isVisible)
-
- panels["control"].page = page + 1
-
- # TODO: this redraw doesn't seem necessary (redraws anyway after this
- # loop) - look into this when refactoring
- panels["control"].redraw(True)
-
- selectiveRefresh(panels, page)
- elif key == ord('p') or key == ord('P'):
- # toggles update freezing
- panel.CURSES_LOCK.acquire()
- try:
- isPaused = not isPaused
- setPauseState(panels, isPaused, page)
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- finally:
- panel.CURSES_LOCK.release()
-
- selectiveRefresh(panels, page)
- elif key == ord('x') or key == ord('X'):
- # provides prompt to confirm that arm should issue a sighup
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("This will reset Tor's internal state. Are you sure (x again to confirm)?", curses.A_BOLD)
- panels["control"].redraw(True)
-
- curses.cbreak()
- confirmationKey = stdscr.getch()
- if confirmationKey in (ord('x'), ord('X')):
- try:
- torTools.getConn().reload()
- except IOError, exc:
- log.log(log.ERR, "Error detected when reloading tor: %s" % sysTools.getFileErrorMsg(exc))
-
- #errorMsg = " (%s)" % str(err) if str(err) else ""
- #panels["control"].setMsg("Sighup failed%s" % errorMsg, curses.A_STANDOUT)
- #panels["control"].redraw(True)
- #time.sleep(2)
-
- # reverts display settings
- curses.halfdelay(REFRESH_RATE * 10)
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
- elif key == ord('h') or key == ord('H'):
- # displays popup for current page's controls
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # lists commands
- popup = panels["popup"]
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Page %i Commands:" % (page + 1), curses.A_STANDOUT)
-
- pageOverrideKeys = ()
-
- if page == 0:
- graphedStats = panels["graph"].currentDisplay
- if not graphedStats: graphedStats = "none"
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll log up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll log down a line")
- popup.addfstr(2, 2, "<b>m</b>: increase graph size")
- popup.addfstr(2, 41, "<b>n</b>: decrease graph size")
- popup.addfstr(3, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
- popup.addfstr(3, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
- popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % panels["graph"].bounds.lower())
- popup.addfstr(4, 41, "<b>a</b>: save snapshot of the log")
- popup.addfstr(5, 2, "<b>e</b>: change logged events")
-
- regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
- popup.addfstr(5, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
-
- hiddenEntryLabel = "visible" if panels["log"].showDuplicates else "hidden"
- popup.addfstr(6, 2, "<b>u</b>: duplicate log entries (<b>%s</b>)" % hiddenEntryLabel)
- popup.addfstr(6, 41, "<b>c</b>: clear event log")
-
- pageOverrideKeys = (ord('m'), ord('n'), ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'), ord('x'))
- if page == 1:
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
- popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
- popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-
- popup.addfstr(3, 2, "<b>enter</b>: edit configuration option")
- popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
-
- listingType = panels["conn"]._listingType.lower()
- popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
-
- popup.addfstr(4, 41, "<b>s</b>: sort ordering")
-
- resolverUtil = connections.getResolver("tor").overwriteResolver
- if resolverUtil == None: resolverUtil = "auto"
- popup.addfstr(5, 2, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
-
- pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('u'))
- elif page == 2:
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
- popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
- popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-
- strippingLabel = "on" if panels["torrc"].stripComments else "off"
- popup.addfstr(3, 2, "<b>s</b>: comment stripping (<b>%s</b>)" % strippingLabel)
-
- lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
- popup.addfstr(3, 41, "<b>n</b>: line numbering (<b>%s</b>)" % lineNumLabel)
-
- popup.addfstr(4, 2, "<b>r</b>: reload torrc")
- popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
- elif page == 3:
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
- popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
- popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
- popup.addfstr(3, 2, "<b>enter</b>: connection details")
-
- popup.addstr(7, 2, "Press any key...")
- popup.refresh()
-
- # waits for user to hit a key, if it belongs to a command then executes it
- curses.cbreak()
- helpExitKey = stdscr.getch()
- if helpExitKey in pageOverrideKeys: overrideKey = helpExitKey
- curses.halfdelay(REFRESH_RATE * 10)
-
- setPauseState(panels, isPaused, page)
- selectiveRefresh(panels, page)
- finally:
- panel.CURSES_LOCK.release()
- elif page == 0 and (key == ord('s') or key == ord('S')):
- # provides menu to pick stats to be graphed
- #options = ["None"] + [label for label in panels["graph"].stats.keys()]
- options = ["None"]
-
- # appends stats labels with first letters of each word capitalized
- initialSelection, i = -1, 1
- if not panels["graph"].currentDisplay: initialSelection = 0
- graphLabels = panels["graph"].stats.keys()
- graphLabels.sort()
- for label in graphLabels:
- if label == panels["graph"].currentDisplay: initialSelection = i
- words = label.split()
- options.append(" ".join(word[0].upper() + word[1:] for word in words))
- i += 1
-
- # hides top label of the graph panel and pauses panels
- if panels["graph"].currentDisplay:
- panels["graph"].showLabel = False
- panels["graph"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
-
- # reverts changes made for popup
- panels["graph"].showLabel = True
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and selection != initialSelection:
- if selection == 0: panels["graph"].setStats(None)
- else: panels["graph"].setStats(options[selection].lower())
-
- selectiveRefresh(panels, page)
-
- # TODO: this shouldn't be necessary with the above refresh, but doesn't seem responsive otherwise...
- panels["graph"].redraw(True)
- elif page == 0 and (key == ord('i') or key == ord('I')):
- # provides menu to pick graph panel update interval
- options = [label for (label, intervalTime) in graphing.graphPanel.UPDATE_INTERVALS]
-
- initialSelection = panels["graph"].updateInterval
-
- #initialSelection = -1
- #for i in range(len(options)):
- # if options[i] == panels["graph"].updateInterval: initialSelection = i
-
- # hides top label of the graph panel and pauses panels
- if panels["graph"].currentDisplay:
- panels["graph"].showLabel = False
- panels["graph"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
-
- # reverts changes made for popup
- panels["graph"].showLabel = True
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1: panels["graph"].updateInterval = selection
-
- selectiveRefresh(panels, page)
- elif page == 0 and (key == ord('b') or key == ord('B')):
- # uses the next boundary type for graph
- panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
-
- selectiveRefresh(panels, page)
- elif page == 0 and (key == ord('a') or key == ord('A')):
- # allow user to enter a path to take a snapshot - abandons if left blank
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("Path to save log snapshot: ")
- panels["control"].redraw(True)
-
- # gets user input (this blocks monitor updates)
- pathInput = panels["control"].getstr(0, 27)
-
- if pathInput:
- try:
- panels["log"].saveSnapshot(pathInput)
- panels["control"].setMsg("Saved: %s" % pathInput, curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
- except IOError, exc:
- panels["control"].setMsg("Unable to save snapshot: %s" % sysTools.getFileErrorMsg(exc), curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
-
- panels["graph"].redraw(True)
- elif page == 0 and (key == ord('e') or key == ord('E')):
- # allow user to enter new types of events to log - unchanged if left blank
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("Events to log: ")
- panels["control"].redraw(True)
-
- # lists event types
- popup = panels["popup"]
- popup.height = 11
- popup.recreate(stdscr, 80)
-
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
- lineNum = 1
- for line in logPanel.EVENT_LISTING.split("\n"):
- line = line[6:]
- popup.addstr(lineNum, 1, line)
- lineNum += 1
- popup.refresh()
-
- # gets user input (this blocks monitor updates)
- eventsInput = panels["control"].getstr(0, 15)
- if eventsInput: eventsInput = eventsInput.replace(' ', '') # strips spaces
-
- # it would be nice to quit on esc, but looks like this might not be possible...
- if eventsInput:
- try:
- expandedEvents = logPanel.expandEvents(eventsInput)
- loggedEvents = setEventListening(expandedEvents, isBlindMode)
- panels["log"].setLoggedEvents(loggedEvents)
- except ValueError, exc:
- panels["control"].setMsg("Invalid flags: %s" % str(exc), curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
-
- # reverts popup dimensions
- popup.height = 9
- popup.recreate(stdscr, 80)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
-
- panels["graph"].redraw(True)
- elif page == 0 and (key == ord('f') or key == ord('F')):
- # provides menu to pick previous regular expression filters or to add a new one
- # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
- options = ["None"] + regexFilters + ["New..."]
- initialSelection = 0 if not panels["log"].regexFilter else 1
-
- # hides top label of the graph panel and pauses panels
- if panels["graph"].currentDisplay:
- panels["graph"].showLabel = False
- panels["graph"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
-
- # applies new setting
- if selection == 0:
- panels["log"].setFilter(None)
- elif selection == len(options) - 1:
- # selected 'New...' option - prompt user to input regular expression
- panel.CURSES_LOCK.acquire()
- try:
- # provides prompt
- panels["control"].setMsg("Regular expression: ")
- panels["control"].redraw(True)
-
- # gets user input (this blocks monitor updates)
- regexInput = panels["control"].getstr(0, 20)
-
- if regexInput:
- try:
- panels["log"].setFilter(re.compile(regexInput))
- if regexInput in regexFilters: regexFilters.remove(regexInput)
- regexFilters = [regexInput] + regexFilters
- except re.error, exc:
- panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- finally:
- panel.CURSES_LOCK.release()
- elif selection != -1:
- try:
- panels["log"].setFilter(re.compile(regexFilters[selection - 1]))
-
- # move selection to top
- regexFilters = [regexFilters[selection - 1]] + regexFilters
- del regexFilters[selection]
- except re.error, exc:
- # shouldn't happen since we've already checked validity
- log.log(log.WARN, "Invalid regular expression ('%s': %s) - removing from listing" % (regexFilters[selection - 1], str(exc)))
- del regexFilters[selection - 1]
-
- if len(regexFilters) > MAX_REGEX_FILTERS: del regexFilters[MAX_REGEX_FILTERS:]
-
- # reverts changes made for popup
- panels["graph"].showLabel = True
- setPauseState(panels, isPaused, page)
- panels["graph"].redraw(True)
- elif page == 0 and key in (ord('n'), ord('N'), ord('m'), ord('M')):
- # Unfortunately modifier keys don't work with the up/down arrows (sending
- # multiple keycodes. The only exception to this is shift + left/right,
- # but for now just gonna use standard characters.
-
- if key in (ord('n'), ord('N')):
- panels["graph"].setGraphHeight(panels["graph"].graphHeight - 1)
- else:
- # don't grow the graph if it's already consuming the whole display
- # (plus an extra line for the graph/log gap)
- maxHeight = panels["graph"].parent.getmaxyx()[0] - panels["graph"].top
- currentHeight = panels["graph"].getHeight()
-
- if currentHeight < maxHeight + 1:
- panels["graph"].setGraphHeight(panels["graph"].graphHeight + 1)
- elif page == 0 and (key == ord('c') or key == ord('C')):
- # provides prompt to confirm that arm should clear the log
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("This will clear the log. Are you sure (c again to confirm)?", curses.A_BOLD)
- panels["control"].redraw(True)
-
- curses.cbreak()
- confirmationKey = stdscr.getch()
- if confirmationKey in (ord('c'), ord('C')): panels["log"].clear()
-
- # reverts display settings
- curses.halfdelay(REFRESH_RATE * 10)
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
- elif page == 1 and (key == ord('u') or key == ord('U')):
- # provides menu to pick identification resolving utility
- options = ["auto"] + connections.Resolver.values()
-
- currentOverwrite = connections.getResolver("tor").overwriteResolver # enums correspond to indices
- if currentOverwrite == None: initialSelection = 0
- else: initialSelection = options.index(currentOverwrite)
-
- # hides top label of conn panel and pauses panels
- panelTitle = panels["conn"]._title
- panels["conn"]._title = ""
- panels["conn"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
- selectedOption = options[selection] if selection != "auto" else None
-
- # reverts changes made for popup
- panels["conn"]._title = panelTitle
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and selectedOption != connections.getResolver("tor").overwriteResolver:
- connections.getResolver("tor").overwriteResolver = selectedOption
- elif page == 1 and key in (ord('d'), ord('D')):
- # presents popup for raw consensus data
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
- panelTitle = panels["conn"]._title
- panels["conn"]._title = ""
- panels["conn"].redraw(True)
-
- descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, panels["conn"])
-
- panels["conn"]._title = panelTitle
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
- elif page == 1 and (key == ord('l') or key == ord('L')):
- # provides a menu to pick the primary information we list connections by
- options = interface.connections.entries.ListingType.values()
-
- # dropping the HOSTNAME listing type until we support displaying that content
- options.remove(interface.connections.entries.ListingType.HOSTNAME)
-
- initialSelection = options.index(panels["conn"]._listingType)
-
- # hides top label of connection panel and pauses the display
- panelTitle = panels["conn"]._title
- panels["conn"]._title = ""
- panels["conn"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
-
- # reverts changes made for popup
- panels["conn"]._title = panelTitle
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and options[selection] != panels["conn"]._listingType:
- panels["conn"].setListingType(options[selection])
- panels["conn"].redraw(True)
- elif page == 1 and (key == ord('s') or key == ord('S')):
- # set ordering for connection options
- titleLabel = "Connection Ordering:"
- options = interface.connections.entries.SortAttr.values()
- oldSelection = panels["conn"]._sortOrdering
- optionColors = dict([(attr, interface.connections.entries.SORT_COLORS[attr]) for attr in options])
- results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
-
- if results:
- panels["conn"].setSortOrder(results)
-
- panels["conn"].redraw(True)
- elif page == 2 and (key == ord('c') or key == ord('C')) and False:
- # TODO: disabled for now (probably gonna be going with separate pages
- # rather than popup menu)
- # provides menu to pick config being displayed
- #options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
- options = []
- initialSelection = panels["torrc"].configType
-
- # hides top label of the graph panel and pauses panels
- panels["torrc"].showLabel = False
- panels["torrc"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Configuration:", options, initialSelection)
-
- # reverts changes made for popup
- panels["torrc"].showLabel = True
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1: panels["torrc"].setConfigType(selection)
-
- selectiveRefresh(panels, page)
- elif page == 2 and (key == ord('w') or key == ord('W')):
- # display a popup for saving the current configuration
- panel.CURSES_LOCK.acquire()
- try:
- configLines = torConfig.getCustomOptions(True)
-
- # lists event types
- popup = panels["popup"]
- popup.height = len(configLines) + 3
- popup.recreate(stdscr)
- displayHeight, displayWidth = panels["popup"].getPreferredSize()
-
- # displayed options (truncating the labels if there's limited room)
- if displayWidth >= 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
- lastIndex = min(displayHeight - 3, len(configLines) - 1)
- isOptionLineSeparate = displayWidth < (30 + len(configLines[lastIndex]))
-
- # if we're showing all the content and have room to display selection
- # options besides the text then shrink the popup by a row
- if not isOptionLineSeparate and displayHeight == len(configLines) + 3:
- popup.height -= 1
- popup.recreate(stdscr)
-
- key, selection = 0, 2
- while not uiTools.isSelectionKey(key):
- # if the popup has been resized then recreate it (needed for the
- # proper border height)
- newHeight, newWidth = panels["popup"].getPreferredSize()
- if (displayHeight, displayWidth) != (newHeight, newWidth):
- displayHeight, displayWidth = newHeight, newWidth
- popup.recreate(stdscr)
-
- # if there isn't room to display the popup then cancel it
- if displayHeight <= 2:
- selection = 2
- break
-
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
-
- visibleConfigLines = displayHeight - 3 if isOptionLineSeparate else displayHeight - 2
- for i in range(visibleConfigLines):
- line = uiTools.cropStr(configLines[i], displayWidth - 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 'T' between the lower left and the covered panel's scroll bar
- if displayWidth > 1: popup.win.addch(displayHeight - 1, 1, curses.ACS_TTEE)
-
- # draws selection options (drawn right to left)
- drawX = displayWidth - 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(displayHeight - 2, drawX, "[")
- popup.addstr(displayHeight - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
- popup.addstr(displayHeight - 2, drawX + len(optionLabel) + 1, "]")
-
- drawX -= 1 # space gap between the options
-
- popup.refresh()
-
- key = stdscr.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 = torConfig.getTorrc()
- try: configLocation = loadedTorrc.getConfigLocation()
- except IOError: configLocation = ""
-
- if selection == 1:
- # prompts user for a configuration location
- promptMsg = "Save to (esc to cancel): "
- panels["control"].setMsg(promptMsg)
- panels["control"].redraw(True)
- configLocation = panels["control"].getstr(0, len(promptMsg), configLocation)
- if configLocation: configLocation = os.path.abspath(configLocation)
-
- if configLocation:
- try:
- # make dir if the path doesn't already exist
- baseDir = os.path.dirname(configLocation)
- if not os.path.exists(baseDir): os.makedirs(baseDir)
-
- # saves the configuration to the file
- configFile = open(configLocation, "w")
- configFile.write("\n".join(configLines))
- configFile.close()
-
- # reloads the cached torrc if overwriting it
- if configLocation == loadedTorrc.getConfigLocation():
- try:
- loadedTorrc.load()
- panels["torrc"]._lastContentHeightArgs = None
- except IOError: pass
-
- msg = "Saved configuration to %s" % configLocation
- except (IOError, OSError), exc:
- msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
-
- panels["control"].setMsg(msg, curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-
- # reverts popup dimensions
- popup.height = 9
- popup.recreate(stdscr, 80)
- finally:
- panel.CURSES_LOCK.release()
-
- panels["config"].redraw(True)
- elif page == 2 and (key == ord('s') or key == ord('S')):
- # set ordering for config options
- titleLabel = "Config Option Ordering:"
- options = [configPanel.FIELD_ATTR[field][0] for field in configPanel.Field.values()]
- oldSelection = [configPanel.FIELD_ATTR[field][0] for field in panels["config"].sortOrdering]
- optionColors = dict([configPanel.FIELD_ATTR[field] for field in configPanel.Field.values()])
- results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
-
- if results:
- # converts labels back to enums
- resultEnums = []
-
- for label in results:
- for entryEnum in configPanel.FIELD_ATTR:
- if label == configPanel.FIELD_ATTR[entryEnum][0]:
- resultEnums.append(entryEnum)
- break
-
- panels["config"].setSortOrder(resultEnums)
-
- panels["config"].redraw(True)
- elif page == 2 and uiTools.isSelectionKey(key):
- # let the user edit the configuration value, unchanged if left blank
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- selection = panels["config"].getSelection()
- configOption = selection.get(configPanel.Field.OPTION)
- titleMsg = "%s Value (esc to cancel): " % configOption
- panels["control"].setMsg(titleMsg)
- panels["control"].redraw(True)
-
- displayWidth = panels["control"].getPreferredSize()[1]
- initialValue = selection.get(configPanel.Field.VALUE)
-
- # initial input for the text field
- initialText = ""
- if CONFIG["features.config.prepopulateEditValues"] and initialValue != "<none>":
- initialText = initialValue
-
- newConfigValue = panels["control"].getstr(0, len(titleMsg), initialText)
-
- # it would be nice to quit on esc, but looks like this might not be possible...
- if newConfigValue != None and newConfigValue != initialValue:
- conn = torTools.getConn()
-
- # if the value's a boolean then allow for 'true' and 'false' inputs
- if selection.get(configPanel.Field.TYPE) == "Boolean":
- if newConfigValue.lower() == "true": newConfigValue = "1"
- elif newConfigValue.lower() == "false": newConfigValue = "0"
-
- try:
- if selection.get(configPanel.Field.TYPE) == "LineList":
- newConfigValue = newConfigValue.split(",")
-
- conn.setOption(configOption, newConfigValue)
-
- # resets the isDefault flag
- customOptions = torConfig.getCustomOptions()
- selection.fields[configPanel.Field.IS_DEFAULT] = not configOption in customOptions
-
- panels["config"].redraw(True)
- except Exception, exc:
- errorMsg = "%s (press any key)" % exc
- panels["control"].setMsg(uiTools.cropStr(errorMsg, displayWidth), curses.A_STANDOUT)
- panels["control"].redraw(True)
-
- curses.cbreak() # wait indefinitely for key presses (no timeout)
- stdscr.getch()
- curses.halfdelay(REFRESH_RATE * 10)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
- elif page == 3 and key == ord('r') or key == ord('R'):
- # reloads torrc, providing a notice if successful or not
- loadedTorrc = torConfig.getTorrc()
- loadedTorrc.getLock().acquire()
-
- try:
- loadedTorrc.load()
- isSuccessful = True
- except IOError:
- isSuccessful = False
-
- loadedTorrc.getLock().release()
-
- #isSuccessful = panels["torrc"].loadConfig(logErrors = False)
- #confTypeLabel = confPanel.CONFIG_LABELS[panels["torrc"].configType]
- resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
- if isSuccessful:
- panels["torrc"]._lastContentHeightArgs = None
- panels["torrc"].redraw(True)
-
- panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(1)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- elif page == 0:
- panels["log"].handleKey(key)
- elif page == 1:
- panels["conn"].handleKey(key)
- elif page == 2:
- panels["config"].handleKey(key)
- elif page == 3:
- panels["torrc"].handleKey(key)
-
-def startTorMonitor(startTime, loggedEvents, isBlindMode):
- try:
- curses.wrapper(drawTorMonitor, startTime, loggedEvents, isBlindMode)
- except KeyboardInterrupt:
- pass # skip printing stack trace in case of keyboard interrupt
-
diff --git a/src/interface/descriptorPopup.py b/src/interface/descriptorPopup.py
deleted file mode 100644
index cdc959d..0000000
--- a/src/interface/descriptorPopup.py
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/env python
-# descriptorPopup.py -- popup panel used to show raw consensus data
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import math
-import socket
-import curses
-from TorCtl import TorCtl
-
-import controller
-import connections.connEntry
-from util import panel, torTools, uiTools
-
-# field keywords used to identify areas for coloring
-LINE_NUM_COLOR = "yellow"
-HEADER_COLOR = "cyan"
-HEADER_PREFIX = ["ns/id/", "desc/id/"]
-
-SIG_COLOR = "red"
-SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
-SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
-
-UNRESOLVED_MSG = "No consensus data available"
-ERROR_MSG = "Unable to retrieve data"
-
-class PopupProperties:
- """
- State attributes of popup window for consensus descriptions.
- """
-
- def __init__(self):
- self.fingerprint = ""
- self.entryColor = "white"
- self.text = []
- self.scroll = 0
- self.showLineNum = True
-
- def reset(self, fingerprint, entryColor):
- self.fingerprint = fingerprint
- self.entryColor = entryColor
- self.text = []
- self.scroll = 0
-
- if fingerprint == "UNKNOWN":
- self.fingerprint = None
- self.showLineNum = False
- self.text.append(UNRESOLVED_MSG)
- else:
- conn = torTools.getConn()
-
- try:
- self.showLineNum = True
- self.text.append("ns/id/%s" % fingerprint)
- self.text += conn.getConsensusEntry(fingerprint).split("\n")
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
- self.text = self.text + [ERROR_MSG, ""]
-
- try:
- descCommand = "desc/id/%s" % fingerprint
- self.text.append("desc/id/%s" % fingerprint)
- self.text += conn.getDescriptorEntry(fingerprint).split("\n")
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
- self.text = self.text + [ERROR_MSG]
-
- def handleKey(self, key, height):
- if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
- elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.text) - height))
- elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - height, 0)
- elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + height, len(self.text) - height))
-
-def showDescriptorPopup(popup, stdscr, connectionPanel):
- """
- Presents consensus descriptor in popup window with the following controls:
- Up, Down, Page Up, Page Down - scroll descriptor
- Right, Left - next / previous connection
- Enter, Space, d, D - close popup
- """
-
- properties = PopupProperties()
- isVisible = True
-
- if not panel.CURSES_LOCK.acquire(False): return
- try:
- while isVisible:
- selection = connectionPanel._scroller.getCursorSelection(connectionPanel._entryLines)
- if not selection: break
- fingerprint = selection.foreign.getFingerprint()
- entryColor = connections.connEntry.CATEGORY_COLOR[selection.getType()]
- properties.reset(fingerprint, entryColor)
-
- # constrains popup size to match text
- width, height = 0, 0
- for line in properties.text:
- # width includes content, line number field, and border
- lineWidth = len(line) + 5
- if properties.showLineNum: lineWidth += int(math.log10(len(properties.text))) + 1
- width = max(width, lineWidth)
-
- # tracks number of extra lines that will be taken due to text wrap
- height += (lineWidth - 2) / connectionPanel.maxX
-
- popup.setHeight(min(len(properties.text) + height + 2, connectionPanel.maxY))
- popup.recreate(stdscr, width)
-
- while isVisible:
- draw(popup, properties)
- key = stdscr.getch()
-
- if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')):
- # closes popup
- isVisible = False
- elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
- # navigation - pass on to connPanel and recreate popup
- connectionPanel.handleKey(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN)
- break
- else: properties.handleKey(key, popup.height - 2)
-
- popup.setHeight(9)
- popup.recreate(stdscr, 80)
- finally:
- panel.CURSES_LOCK.release()
-
-def draw(popup, properties):
- popup.clear()
- popup.win.box()
- xOffset = 2
-
- if properties.text:
- if properties.fingerprint: popup.addstr(0, 0, "Consensus Descriptor (%s):" % properties.fingerprint, curses.A_STANDOUT)
- else: popup.addstr(0, 0, "Consensus Descriptor:", curses.A_STANDOUT)
-
- isEncryption = False # true if line is part of an encryption block
-
- # checks if first line is in an encryption block
- for i in range(0, properties.scroll):
- lineText = properties.text[i].strip()
- if lineText in SIG_START_KEYS: isEncryption = True
- elif lineText in SIG_END_KEYS: isEncryption = False
-
- pageHeight = popup.maxY - 2
- numFieldWidth = int(math.log10(len(properties.text))) + 1
- lineNum = 1
- for i in range(properties.scroll, min(len(properties.text), properties.scroll + pageHeight)):
- lineText = properties.text[i].strip()
-
- numOffset = 0 # offset for line numbering
- if properties.showLineNum:
- popup.addstr(lineNum, xOffset, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR))
- numOffset = numFieldWidth + 1
-
- if lineText:
- keyword = lineText.split()[0] # first word of line
- remainder = lineText[len(keyword):]
- keywordFormat = curses.A_BOLD | uiTools.getColor(properties.entryColor)
- remainderFormat = uiTools.getColor(properties.entryColor)
-
- if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]):
- keyword, remainder = lineText, ""
- keywordFormat = curses.A_BOLD | uiTools.getColor(HEADER_COLOR)
- if lineText == UNRESOLVED_MSG or lineText == ERROR_MSG:
- keyword, remainder = lineText, ""
- if lineText in SIG_START_KEYS:
- keyword, remainder = lineText, ""
- isEncryption = True
- keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
- elif lineText in SIG_END_KEYS:
- keyword, remainder = lineText, ""
- isEncryption = False
- keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
- elif isEncryption:
- keyword, remainder = lineText, ""
- keywordFormat = uiTools.getColor(SIG_COLOR)
-
- lineNum, xLoc = controller.addstr_wrap(popup, lineNum, 0, keyword, keywordFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
- lineNum, xLoc = controller.addstr_wrap(popup, lineNum, xLoc, remainder, remainderFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
-
- lineNum += 1
- if lineNum > pageHeight: break
-
- popup.refresh()
-
diff --git a/src/interface/graphing/__init__.py b/src/interface/graphing/__init__.py
deleted file mode 100644
index 9a81dbd..0000000
--- a/src/interface/graphing/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Panels, popups, and handlers comprising the arm user interface.
-"""
-
-__all__ = ["graphPanel", "bandwidthStats", "connStats", "resourceStats"]
-
diff --git a/src/interface/graphing/bandwidthStats.py b/src/interface/graphing/bandwidthStats.py
deleted file mode 100644
index f8e3020..0000000
--- a/src/interface/graphing/bandwidthStats.py
+++ /dev/null
@@ -1,398 +0,0 @@
-"""
-Tracks bandwidth usage of the tor process, expanding to include accounting
-stats if they're set.
-"""
-
-import time
-
-from interface.graphing import graphPanel
-from util import log, sysTools, torTools, uiTools
-
-DL_COLOR, UL_COLOR = "green", "cyan"
-
-# width at which panel abandons placing optional stats (avg and total) with
-# header in favor of replacing the x-axis label
-COLLAPSE_WIDTH = 135
-
-# valid keys for the accountingInfo mapping
-ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writtenLimit")
-
-PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file"
-PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
-
-DEFAULT_CONFIG = {"features.graph.bw.transferInBytes": False,
- "features.graph.bw.accounting.show": True,
- "features.graph.bw.accounting.rate": 10,
- "features.graph.bw.accounting.isTimeLong": False,
- "log.graph.bw.prepopulateSuccess": log.NOTICE,
- "log.graph.bw.prepopulateFailure": log.NOTICE}
-
-class BandwidthStats(graphPanel.GraphStats):
- """
- Uses tor BW events to generate bandwidth usage graph.
- """
-
- def __init__(self, config=None):
- graphPanel.GraphStats.__init__(self)
-
- self._config = dict(DEFAULT_CONFIG)
- if config:
- config.update(self._config, {"features.graph.bw.accounting.rate": 1})
-
- # stats prepopulated from tor's state file
- self.prepopulatePrimaryTotal = 0
- self.prepopulateSecondaryTotal = 0
- 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
- self.resetListener(conn, torTools.State.INIT) # 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")
- if readTotal and readTotal.isdigit():
- self.initialPrimaryTotal = int(readTotal) / 1024 # Bytes -> KB
-
- writeTotal = conn.getInfo("traffic/written")
- if writeTotal and writeTotal.isdigit():
- self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
-
- def resetListener(self, conn, 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 == torTools.State.INIT and self._config["features.graph.bw.accounting.show"]:
- self.isAccounting = conn.getInfo('accounting/enabled') == '1'
-
- 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")
- if orPort == "0": return
-
- # gets the uptime (using the same parameters as the header panel to take
- # advantage of caching
- uptime = None
- queryPid = conn.getMyPid()
- if queryPid:
- queryParam = ["%cpu", "rss", "%mem", "etime"]
- queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
- psCall = sysTools.call(queryCmd, 3600, True)
-
- 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
- if not uptime or not "-" in uptime:
- msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
- log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
- return False
-
- # get the user's data directory (usually '~/.tor')
- dataDir = conn.getOption("DataDirectory")
- if not dataDir:
- msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
- log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
- return False
-
- # attempt to open the state file
- try: stateFile = open("%s%s/state" % (conn.getPathPrefix(), dataDir), "r")
- except IOError:
- msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
- log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
- 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]
- bwReadEntries.pop()
- elif line.startswith("BWHistoryWriteValues"):
- bwWriteEntries = line[21:].split(",")
- bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
- bwWriteEntries.pop()
- elif line.startswith("BWHistoryReadEnds"):
- lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
- lastReadTime -= 900
- missingReadEntries = int((time.time() - lastReadTime) / 900)
- elif line.startswith("BWHistoryWriteEnds"):
- 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.log(self._config["log.graph.bw.prepopulateFailure"], 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)" % uiTools.getTimeLabel(missingSec, 0, True)
- log.log(self._config["log.graph.bw.prepopulateSuccess"], msg)
-
- return True
-
- def bandwidth_event(self, event):
- if self.isAccounting and self.isNextTickRedraw():
- if time.time() - self.accountingLastUpdated >= self._config["features.graph.bw.accounting.rate"]:
- 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.addfstr(labelingLine + 2, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
-
- 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.addfstr(labelingLine + 2, 0, "<b>Accounting:</b> 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" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"]))
-
- # drops label's components if there's not enough space
- labeling = graphType + " (" + "".join(stats).strip() + "):"
- while len(labeling) >= width:
- if len(stats) > 1:
- del stats[-1]
- labeling = graphType + " (" + "".join(stats).strip() + "):"
- 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")
- if not self._titleStats or not myFingerprint or (event and myFingerprint in event.idlist):
- stats = []
- bwRate = conn.getMyBandwidthRate()
- bwBurst = conn.getMyBandwidthBurst()
- bwObserved = conn.getMyBandwidthObserved()
- bwMeasured = conn.getMyBandwidthMeasured()
- labelInBytes = self._config["features.graph.bw.transferInBytes"]
-
- if bwRate and bwBurst:
- bwRateLabel = uiTools.getSizeLabel(bwRate, 1, False, labelInBytes)
- bwBurstLabel = uiTools.getSizeLabel(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).
- if bwObserved and (not bwMeasured or bwMeasured == bwObserved):
- stats.append("observed: %s/s" % uiTools.getSizeLabel(bwObserved, 1, False, labelInBytes))
- elif bwMeasured:
- stats.append("measured: %s/s" % uiTools.getSizeLabel(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" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, self._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" % uiTools.getSizeLabel(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")
-
- # provides a nicely formatted reset time
- endInterval = conn.getInfo("accounting/interval-end")
- 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 self._config["features.graph.bw.accounting.isTimeLong"]:
- queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True))
- else:
- days = sec / 86400
- sec %= 86400
- hours = sec / 3600
- sec %= 3600
- 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")
- left = conn.getInfo("accounting/bytes-left")
-
- 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"] = uiTools.getSizeLabel(read)
- queried["written"] = uiTools.getSizeLabel(written)
- queried["readLimit"] = uiTools.getSizeLabel(read + readLeft)
- queried["writtenLimit"] = uiTools.getSizeLabel(written + writtenLeft)
-
- self.accountingInfo = queried
- self.accountingLastUpdated = time.time()
-
diff --git a/src/interface/graphing/connStats.py b/src/interface/graphing/connStats.py
deleted file mode 100644
index 511490c..0000000
--- a/src/interface/graphing/connStats.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""
-Tracks stats concerning tor's current connections.
-"""
-
-from interface.graphing import graphPanel
-from util import connections, torTools
-
-class ConnStats(graphPanel.GraphStats):
- """
- 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, torTools.State.INIT) # initialize port values
- conn.addStatusListener(self.resetListener)
-
- def resetListener(self, conn, eventType):
- if eventType == torTools.State.INIT:
- self.orPort = conn.getOption("ORPort", "0")
- self.dirPort = conn.getOption("DirPort", "0")
- self.controlPort = conn.getOption("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/src/interface/graphing/graphPanel.py b/src/interface/graphing/graphPanel.py
deleted file mode 100644
index e4b493d..0000000
--- a/src/interface/graphing/graphPanel.py
+++ /dev/null
@@ -1,407 +0,0 @@
-"""
-Flexible panel for presenting bar graphs for a variety of stats. This panel is
-just concerned with the rendering of information, which is actually collected
-and stored by implementations of the GraphStats interface. Panels are made up
-of a title, followed by headers and graphs for two sets of stats. For
-instance...
-
-Bandwidth (cap: 5 MB, burst: 10 MB):
-Downloaded (0.0 B/sec): Uploaded (0.0 B/sec):
- 34 30
- * *
- ** * * * **
- * * * ** ** ** *** ** ** ** **
- ********* ****** ****** ********* ****** ******
- 0 ************ **************** 0 ************ ****************
- 25s 50 1m 1.6 2.0 25s 50 1m 1.6 2.0
-"""
-
-import copy
-import curses
-from TorCtl import TorCtl
-
-from util import enum, panel, uiTools
-
-# time intervals at which graphs can be updated
-UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5), ("30 seconds", 30),
- ("minutely", 60), ("15 minute", 900), ("30 minute", 1800),
- ("hourly", 3600), ("daily", 86400)]
-
-DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
-DEFAULT_COLOR_PRIMARY, DEFAULT_COLOR_SECONDARY = "green", "cyan"
-MIN_GRAPH_HEIGHT = 1
-
-# enums for graph bounds:
-# Bounds.GLOBAL_MAX - global maximum (highest value ever seen)
-# Bounds.LOCAL_MAX - local maximum (highest value currently on the graph)
-# Bounds.TIGHT - local maximum and minimum
-Bounds = enum.Enum("GLOBAL_MAX", "LOCAL_MAX", "TIGHT")
-
-WIDE_LABELING_GRAPH_COL = 50 # minimum graph columns to use wide spacing for x-axis labels
-
-# used for setting defaults when initializing GraphStats and GraphPanel instances
-CONFIG = {"features.graph.height": 7,
- "features.graph.interval": 0,
- "features.graph.bound": 1,
- "features.graph.maxWidth": 150,
- "features.graph.showIntermediateBounds": True}
-
-def loadConfig(config):
- config.update(CONFIG, {
- "features.graph.height": MIN_GRAPH_HEIGHT,
- "features.graph.maxWidth": 1,
- "features.graph.interval": (0, len(UPDATE_INTERVALS) - 1),
- "features.graph.bound": (0, 2)})
-
-class GraphStats(TorCtl.PostEventListener):
- """
- Module that's expected to update dynamically and provide attributes to be
- 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, isPauseBuffer=False):
- """
- Initializes parameters needed to present a graph.
- """
-
- TorCtl.PostEventListener.__init__(self)
-
- # panel to be redrawn when updated (set when added to GraphPanel)
- self._graphPanel = None
-
- # mirror instance used to track updates when paused
- self.isPaused, self.isPauseBuffer = False, isPauseBuffer
- if isPauseBuffer: self._pauseBuffer = None
- else: self._pauseBuffer = GraphStats(True)
-
- # 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]
-
- 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 not self.isPauseBuffer and not self.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 setPaused(self, isPause):
- """
- If true, prevents bandwidth updates from being presented. This is a no-op
- if a pause buffer.
- """
-
- if isPause == self.isPaused or self.isPauseBuffer: return
- self.isPaused = isPause
-
- if self.isPaused: active, inactive = self._pauseBuffer, self
- else: active, inactive = self, self._pauseBuffer
- self._parameterSwap(active, inactive)
-
- def bandwidth_event(self, event):
- self.eventTick()
-
- def _parameterSwap(self, active, inactive):
- """
- Either overwrites parameters of pauseBuffer or with the current values or
- vice versa. This is a helper method for setPaused and should be overwritten
- to append with additional parameters that need to be preserved when paused.
- """
-
- # The pause buffer is constructed as a GraphStats instance which will
- # become problematic if this is overridden by any implementations (which
- # currently isn't the case). If this happens then the pause buffer will
- # need to be of the requester's type (not quite sure how to do this
- # gracefully...).
-
- active.tick = inactive.tick
- active.lastPrimary = inactive.lastPrimary
- active.lastSecondary = inactive.lastSecondary
- active.primaryTotal = inactive.primaryTotal
- active.secondaryTotal = inactive.secondaryTotal
- active.maxPrimary = dict(inactive.maxPrimary)
- active.maxSecondary = dict(inactive.maxSecondary)
- active.primaryCounts = copy.deepcopy(inactive.primaryCounts)
- active.secondaryCounts = copy.deepcopy(inactive.secondaryCounts)
-
- def _processEvent(self, primary, secondary):
- """
- Includes new stats in graphs and notifies associated GraphPanel of changes.
- """
-
- if self.isPaused: self._pauseBuffer._processEvent(primary, secondary)
- else:
- 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: self._graphPanel.redraw(True)
-
-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"]
- self.bounds = Bounds.values()[CONFIG["features.graph.bound"]]
- self.graphHeight = CONFIG["features.graph.height"]
- self.currentDisplay = None # label of the stats currently being displayed
- self.stats = {} # available stats (mappings of label -> instance)
- self.showLabel = True # shows top label if true, hides otherwise
- self.isPaused = False
-
- 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 draw(self, width, height):
- """ Redraws graph panel """
-
- if self.currentDisplay:
- param = self.stats[self.currentDisplay]
- graphCol = min((width - 10) / 2, param.maxCol)
-
- primaryColor = uiTools.getColor(param.getColor(True))
- secondaryColor = uiTools.getColor(param.getColor(False))
-
- if self.showLabel: 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])
- secondaryMaxBound = int(param.maxSecondary[self.updateInterval])
- else:
- # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
- if graphCol < 2:
- # nothing being displayed
- primaryMaxBound, secondaryMaxBound = 0, 0
- 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 - 1) * (self.graphHeight - row - 1)
- if not primaryVal in (primaryMinBound, primaryMaxBound): self.addstr(row + 2, 0, "%4i" % primaryVal, primaryColor)
-
- if secondaryMinBound != secondaryMaxBound:
- secondaryVal = (secondaryMaxBound - secondaryMinBound) / (self.graphHeight - 1) * (self.graphHeight - row - 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 = uiTools.getTimeLabel(loc * intervalSec, decimalPrecision)
-
- if not unitsLabel: unitsLabel = timeLabel[-1]
- elif unitsLabel != timeLabel[-1]:
- # upped scale so also up precision of future measurements
- unitsLabel = timeLabel[-1]
- decimalPrecision += 1
- 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
- stats.isPaused = True
- self.stats[label] = stats
-
- 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].setPaused(True)
-
- if not label:
- self.currentDisplay = None
- elif label in self.stats.keys():
- self.currentDisplay = label
- self.stats[label].setPaused(self.isPaused)
- else: raise ValueError("Unrecognized stats label: %s" % label)
-
- def setPaused(self, isPause):
- """
- If true, prevents bandwidth updates from being presented.
- """
-
- if isPause == self.isPaused: return
- self.isPaused = isPause
- if self.currentDisplay: self.stats[self.currentDisplay].setPaused(self.isPaused)
-
diff --git a/src/interface/graphing/resourceStats.py b/src/interface/graphing/resourceStats.py
deleted file mode 100644
index 864957e..0000000
--- a/src/interface/graphing/resourceStats.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""
-Tracks the system resource usage (cpu and memory) of the tor process.
-"""
-
-from interface.graphing import graphPanel
-from util import sysTools, torTools, uiTools
-
-class ResourceStats(graphPanel.GraphStats):
- """
- System resource usage tracker.
- """
-
- def __init__(self):
- graphPanel.GraphStats.__init__(self)
- self.queryPid = torTools.getConn().getMyPid()
-
- 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:
- # memory sizes are converted from MB to B before generating labels
- usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
- avgLabel = uiTools.getSizeLabel(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)
-
- if 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/src/interface/headerPanel.py b/src/interface/headerPanel.py
deleted file mode 100644
index f653299..0000000
--- a/src/interface/headerPanel.py
+++ /dev/null
@@ -1,474 +0,0 @@
-"""
-Top panel for every page, containing basic system and tor related information.
-If there's room available then this expands to present its information in two
-columns, otherwise it's laid out as follows:
- arm - <hostname> (<os> <sys/version>) Tor <tor/version> (<new, old, recommended, etc>)
- <nickname> - <address>:<orPort>, [Dir Port: <dirPort>, ]Control Port (<open, password, cookie>): <controlPort>
- cpu: <cpu%> mem: <mem> (<mem%>) uid: <uid> uptime: <upmin>:<upsec>
- fingerprint: <fingerprint>
-
-Example:
- arm - odin (Linux 2.6.24-24-generic) Tor 0.2.1.19 (recommended)
- odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
- cpu: 14.6% mem: 42 MB (4.2%) pid: 20060 uptime: 48:27
- fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
-"""
-
-import os
-import time
-import curses
-import threading
-
-from util import log, panel, sysTools, torTools, uiTools
-
-# minimum width for which panel attempts to double up contents (two columns to
-# better use screen real estate)
-MIN_DUAL_COL_WIDTH = 141
-
-FLAG_COLORS = {"Authority": "white", "BadExit": "red", "BadDirectory": "red", "Exit": "cyan",
- "Fast": "yellow", "Guard": "green", "HSDir": "magenta", "Named": "blue",
- "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",
- "old": "red", "unrecommended": "red", "unknown": "cyan"}
-
-DEFAULT_CONFIG = {"features.showFdUsage": False,
- "log.fdUsageSixtyPercent": log.NOTICE,
- "log.fdUsageNinetyPercent": log.WARN}
-
-class HeaderPanel(panel.Panel, threading.Thread):
- """
- Top area contenting tor settings and system information. Stats are stored in
- the vals mapping, keys including:
- tor/ version, versionStatus, nickname, orPort, dirPort, controlPort,
- exitPolicy, isAuthPassword (bool), isAuthCookie (bool),
- orListenAddr, *address, *fingerprint, *flags, pid, startTime,
- *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, config = None):
- panel.Panel.__init__(self, stdscr, "header", 0)
- threading.Thread.__init__(self)
- self.setDaemon(True)
-
- self._config = dict(DEFAULT_CONFIG)
- if config: config.update(self._config)
-
- self._isTorConnected = True
- self._lastUpdate = -1 # time the content was last revised
- self._isPaused = False # prevents updates if true
- 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 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:
- versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \
- self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white"
- versionStatusMsg = "<%s>%s</%s>" % (versionColor, self.vals["tor/versionStatus"], versionColor)
- self.addfstr(0, 43, "Tor %s (%s)" % (self.vals["tor/version"], versionStatusMsg))
- 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)
- 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
- entry = ""
- 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):
- if len(entry) + len(label) <= leftWidth: entry += label
- else: break
- else:
- # non-relay (client only)
- # TODO: not sure what sort of stats to provide...
- entry = "<red><b>Relaying Disabled</b></red>"
-
- if self.vals["tor/isAuthPassword"]: authType = "password"
- elif self.vals["tor/isAuthCookie"]: authType = "cookie"
- else: authType = "open"
-
- if len(entry) + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth:
- authColor = "red" if authType == "open" else "green"
- authLabel = "<%s>%s</%s>" % (authColor, authType, authColor)
- self.addfstr(1, 0, "%s, Control Port (%s): %s" % (entry, authLabel, self.vals["tor/controlPort"]))
- elif len(entry) + 16 + len(self.vals["tor/controlPort"]) <= leftWidth:
- self.addstr(1, 0, "%s, Control Port: %s" % (entry, self.vals["tor/controlPort"]))
- else: self.addstr(1, 0, entry)
-
- # Line 3 / Line 1 Right (system usage info)
- y, x = (0, leftWidth) if isWide else (2, 0)
- if self.vals["stat/rss"] != "0": memoryLabel = uiTools.getSizeLabel(int(self.vals["stat/rss"]))
- else: memoryLabel = "0"
-
- uptimeLabel = ""
- if self.vals["tor/startTime"]:
- if self._haltTime:
- # freeze the uptime when paused or the tor process is stopped
- uptimeLabel = uiTools.getShortTimeLabel(self._haltTime - self.vals["tor/startTime"])
- else:
- uptimeLabel = uiTools.getShortTimeLabel(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 self._config["features.showFdUsage"]:
- fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL
- if fdPercent >= 95:
- fdPercentFormat = curses.A_BOLD | uiTools.getColor("red")
- elif fdPercent >= 90:
- 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:
- flagLine = "flags: "
- for flag in self.vals["tor/flags"]:
- flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white"
- flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor)
-
- if len(self.vals["tor/flags"]) > 0: flagLine = flagLine[:-2]
- else: flagLine += "<b><cyan>none</cyan></b>"
-
- self.addfstr(2 if isWide else 4, 0, flagLine)
- else:
- statusTime = torTools.getConn().getStatus()[1]
- statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime))
- self.addfstr(2 if isWide else 4, 0, "<b><red>Tor Disconnected</red></b> (%s)" % 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>"
-
- # 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()
- displayedPolicy = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy
- if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy
- elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy
- elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy
- policies[i] = policy
-
- self.addfstr(2, leftWidth, "exit policy: %s" % ", ".join(policies))
- else:
- # Client only
- # TODO: not sure what information to provide here...
- pass
-
- self.valsLock.release()
-
- def setPaused(self, isPause):
- """
- If true, prevents updates from being presented.
- """
-
- if not self._isPaused == isPause:
- self._isPaused = isPause
- if self._isTorConnected:
- if isPause: self._haltTime = time.time()
- else: self._haltTime = None
-
- # Redraw now so we'll be displaying the state right when paused
- # (otherwise the uptime might be off by a second, and change when
- # the panel's redrawn for other reasons).
- self.redraw(True)
-
- 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)
- self._cond.release()
- else:
- # Update the volatile attributes (cpu, memory, flags, etc) if we have
- # 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, conn, eventType):
- """
- Updates static parameters on tor reload (sighup) events.
-
- Arguments:
- conn - tor controller
- eventType - type of event detected
- """
-
- if eventType == torTools.State.INIT:
- self._isTorConnected = True
- if self._isPaused: self._haltTime = time.time()
- else: self._haltTime = None
-
- self._update(True)
- self.redraw(True)
- elif eventType == torTools.State.CLOSED:
- self._isTorConnected = False
- 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
- self.vals["tor/version"] = conn.getInfo("version", "Unknown").split()[0]
- self.vals["tor/versionStatus"] = conn.getInfo("status/version/current", "Unknown")
- self.vals["tor/nickname"] = conn.getOption("Nickname", "")
- self.vals["tor/orPort"] = conn.getOption("ORPort", "0")
- self.vals["tor/dirPort"] = conn.getOption("DirPort", "0")
- self.vals["tor/controlPort"] = conn.getOption("ControlPort", "")
- self.vals["tor/isAuthPassword"] = conn.getOption("HashedControlPassword") != None
- self.vals["tor/isAuthCookie"] = conn.getOption("CookieAuthentication") == "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")
- if listenAddr:
- if ":" in listenAddr:
- # both ip and port overwritten
- self.vals["tor/orListenAddr"] = listenAddr[:listenAddr.find(":")]
- 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]
-
- pid = conn.getMyPid()
- self.vals["tor/pid"] = pid if pid else ""
-
- 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"] = []
- self.vals["tor/fdUsed"] = 0
- self.vals["stat/%torCpu"] = "0"
- 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.
- if self.vals["tor/fdLimit"]:
- 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."
- log.log(self._config["log.fdUsageNinetyPercent"], msg)
- elif fdPercent >= 60 and not self._isFdSixtyPercentWarned:
- self._isFdSixtyPercentWarned = True
- log.log(self._config["log.fdUsageSixtyPercent"], 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"
- self.vals["stat/%mem"] = "0"
- else:
- cpuUsage, _, memUsage, memUsagePercent = resourceTracker.getResourceUsage()
- self._lastResourceFetch = resourceTracker.getRunCount()
- 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]
- pythonCpuTime = armCpuDelta / armTimeDelta
- 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/src/interface/logPanel.py b/src/interface/logPanel.py
deleted file mode 100644
index 86e680f..0000000
--- a/src/interface/logPanel.py
+++ /dev/null
@@ -1,1100 +0,0 @@
-"""
-Panel providing a chronological log of events its been configured to listen
-for. This provides prepopulation from the log file and supports filtering by
-regular expressions.
-"""
-
-import time
-import os
-import curses
-import threading
-
-from TorCtl import TorCtl
-
-from version import VERSION
-from util import conf, log, panel, sysTools, torTools, uiTools
-
-TOR_EVENT_TYPES = {
- "d": "DEBUG", "a": "ADDRMAP", "k": "DESCCHANGED", "s": "STREAM",
- "i": "INFO", "f": "AUTHDIR_NEWDESCS", "g": "GUARD", "r": "STREAM_BW",
- "n": "NOTICE", "h": "BUILDTIMEOUT_SET", "l": "NEWCONSENSUS", "t": "STATUS_CLIENT",
- "w": "WARN", "b": "BW", "m": "NEWDESC", "u": "STATUS_GENERAL",
- "e": "ERR", "c": "CIRC", "p": "NS", "v": "STATUS_SERVER",
- "j": "CLIENTS_SEEN", "q": "ORCONN"}
-
-EVENT_LISTING = """ d DEBUG a ADDRMAP k DESCCHANGED s STREAM
- i INFO f AUTHDIR_NEWDESCS g GUARD r STREAM_BW
- n NOTICE h BUILDTIMEOUT_SET l NEWCONSENSUS t STATUS_CLIENT
- w WARN b BW m NEWDESC u STATUS_GENERAL
- e ERR c CIRC p NS v STATUS_SERVER
- j CLIENTS_SEEN q ORCONN
- DINWE tor runlevel+ A All Events
- 12345 arm runlevel+ X No Events
- 67890 torctl runlevel+ U Unknown Events"""
-
-RUNLEVEL_EVENT_COLOR = {log.DEBUG: "magenta", log.INFO: "blue", log.NOTICE: "green",
- log.WARN: "yellow", log.ERR: "red"}
-DAYBREAK_EVENT = "DAYBREAK" # special event for marking when the date changes
-TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
-
-ENTRY_INDENT = 2 # spaces an entry's message is indented after the first line
-DEFAULT_CONFIG = {"features.logFile": "",
- "features.log.showDateDividers": True,
- "features.log.showDuplicateEntries": False,
- "features.log.entryDuration": 7,
- "features.log.maxLinesPerEntry": 4,
- "features.log.prepopulate": True,
- "features.log.prepopulateReadLimit": 5000,
- "features.log.maxRefreshRate": 300,
- "cache.logPanel.size": 1000,
- "log.logPanel.prepopulateSuccess": log.INFO,
- "log.logPanel.prepopulateFailed": log.WARN,
- "log.logPanel.logFileOpened": log.NOTICE,
- "log.logPanel.logFileWriteFailed": log.ERR,
- "log.logPanel.forceDoubleRedraw": log.DEBUG}
-
-DUPLICATE_MSG = " [%i duplicate%s hidden]"
-
-# The height of the drawn content is estimated based on the last time we redrew
-# the panel. It's chiefly used for scrolling and the bar indicating its
-# position. Letting the estimate be too inaccurate results in a display bug, so
-# redraws the display if it's off by this threshold.
-CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
-
-# static starting portion of common log entries, fetched from the config when
-# needed if None
-COMMON_LOG_MESSAGES = None
-
-# cached values and the arguments that generated it for the getDaybreaks and
-# getDuplicates functions
-CACHED_DAYBREAKS_ARGUMENTS = (None, None) # events, current day
-CACHED_DAYBREAKS_RESULT = None
-CACHED_DUPLICATES_ARGUMENTS = None # events
-CACHED_DUPLICATES_RESULT = None
-
-# duration we'll wait for the deduplication function before giving up (in ms)
-DEDUPLICATION_TIMEOUT = 100
-
-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)
-
-def expandEvents(eventAbbr):
- """
- Expands event abbreviations to their full names. Beside mappings provided in
- TOR_EVENT_TYPES this recognizes the following special events and aliases:
- U - UKNOWN events
- A - all events
- X - no events
- DINWE - runlevel and higher
- 12345 - arm runlevel and higher (ARM_DEBUG - ARM_ERR)
- 67890 - torctl runlevel and higher (TORCTL_DEBUG - TORCTL_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.values()]
- torctlRunlevels = ["TORCTL_" + runlevel for runlevel in log.Runlevel.values()]
- expandedEvents = set(TOR_EVENT_TYPES.values() + armRunlevels + torctlRunlevels + ["UNKNOWN"])
- break
- elif flag == "X":
- expandedEvents = set()
- break
- elif flag in "DINWE1234567890":
- # all events for a runlevel and higher
- if flag in "DINWE": typePrefix = ""
- elif flag in "12345": typePrefix = "ARM_"
- elif flag in "67890": typePrefix = "TORCTL_"
-
- if flag in "D16": runlevelIndex = 0
- elif flag in "I27": runlevelIndex = 1
- elif flag in "N38": runlevelIndex = 2
- elif flag in "W49": runlevelIndex = 3
- elif flag in "E50": runlevelIndex = 4
-
- runlevelSet = [typePrefix + runlevel for runlevel in log.Runlevel.values()[runlevelIndex:]]
- expandedEvents = expandedEvents.union(set(runlevelSet))
- elif flag == "U":
- expandedEvents.add("UNKNOWN")
- elif flag in TOR_EVENT_TYPES:
- expandedEvents.add(TOR_EVENT_TYPES[flag])
- else:
- invalidFlags += flag
-
- if invalidFlags: raise ValueError(invalidFlags)
- else: return expandedEvents
-
-def getMissingEventTypes():
- """
- Provides the event types the current torctl connection supports but arm
- 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")
-
- if torEventTypes:
- torEventTypes = torEventTypes.split(" ")
- armEventTypes = TOR_EVENT_TYPES.values()
- return [event for event in torEventTypes if not event in armEventTypes]
- else: return None # GETINFO call failed
-
-def loadLogMessages():
- """
- Fetches a mapping of common log messages to their runlevels from the config.
- """
-
- global COMMON_LOG_MESSAGES
- armConf = conf.getConfig("arm")
-
- COMMON_LOG_MESSAGES = {}
- for confKey in armConf.getKeys():
- if confKey.startswith("msg."):
- eventType = confKey[4:].upper()
- messages = armConf.get(confKey, [])
- COMMON_LOG_MESSAGES[eventType] = messages
-
-def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = None):
- """
- Parses tor's log file for past events matching the given runlevels, providing
- a list of log entries (ordered newest to oldest). Limiting the number of read
- 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)
- config - configuration parameters related to this panel, uses defaults
- if left as None
- """
-
- startTime = time.time()
- if not runlevels: return []
-
- if not config: config = DEFAULT_CONFIG
-
- # checks tor's configuration for the log file's location (if any exists)
- loggingTypes, loggingLocation = None, None
- for loggingEntry in torTools.getConn().getOption("Log", [], True):
- # 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.getConn().getPathPrefix() + loggingLocation
-
- # if the runlevels argument is a superset of the log file then we can
- # limit the read contents to the addLimit
- runlevels = log.Runlevel.values()
- loggingTypes = loggingTypes.upper()
- if addLimit and (not readLimit or readLimit > addLimit):
- if "-" in loggingTypes:
- divIndex = loggingTypes.find("-")
- sIndex = runlevels.index(loggingTypes[:divIndex])
- eIndex = runlevels.index(loggingTypes[divIndex+1:])
- logFileRunlevels = runlevels[sIndex:eIndex+1]
- 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:
- if readLimit:
- lines = sysTools.call("tail -n %i %s" % (readLimit, loggingLocation))
- if not lines: raise IOError()
- else:
- logFile = open(loggingLocation, "r")
- lines = logFile.readlines()
- logFile.close()
- except IOError:
- msg = "Unable to read tor's log file: %s" % loggingLocation
- log.log(config["log.logPanel.prepopulateFailed"], msg)
-
- 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()
- 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(".")]
-
- # overwrites missing time parameters with the local time (ignoring wday
- # and yday since they aren't used)
- eventTimeComp = list(time.strptime(timestamp, "%b %d %H:%M:%S"))
- eventTimeComp[0] = currentLocalTime.tm_year
- 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]
- msg = "Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(loggedEvents), loggingLocation, readLimit, time.time() - startTime)
- log.log(config["log.logPanel.prepopulateSuccess"], msg)
- return loggedEvents
-
-def getDaybreaks(events, ignoreTimeForCache = False):
- """
- Provides the input events back with special 'DAYBREAK_EVENT' markers inserted
- 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):
- """
- Deduplicates a list of log entries, providing back a tuple listing with the
- 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):
- """
- 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
- elif event.type in COMMON_LOG_MESSAGES:
- for commonMsg in COMMON_LOG_MESSAGES[event.type]:
- # if it starts with an asterisk then check the whole message rather
- # than just the start
- if commonMsg[0] == "*":
- 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
-
-class LogEntry():
- """
- Individual log file entry, having the following attributes:
- timestamp - unix timestamp for when the event occurred
- eventType - event type that occurred ("INFO", "BW", "ARM_WARN", etc)
- 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 TorEventObserver(TorCtl.PostEventListener):
- """
- Listens for all types of events provided by TorCtl, providing an LogEntry
- instance to the given callback function.
- """
-
- def __init__(self, callback):
- """
- Tor event listener with the purpose of translating events to nicely
- formatted calls of a callback function.
-
- Arguments:
- callback - function accepting a LogEntry, called when an event of these
- types occur
- """
-
- TorCtl.PostEventListener.__init__(self)
- self.callback = callback
-
- def circ_status_event(self, event):
- msg = "ID: %-3s STATUS: %-10s PATH: %s" % (event.circ_id, event.status, ", ".join(event.path))
- if event.purpose: msg += " PURPOSE: %s" % event.purpose
- if event.reason: msg += " REASON: %s" % event.reason
- if event.remote_reason: msg += " REMOTE_REASON: %s" % event.remote_reason
- self._notify(event, msg, "yellow")
-
- def buildtimeout_set_event(self, event):
- self._notify(event, "SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (event.set_type, event.total_times, event.timeout_ms, event.xm, event.alpha, event.cutoff_quantile))
-
- def stream_status_event(self, event):
- self._notify(event, "ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose))
-
- def or_conn_status_event(self, event):
- msg = "STATUS: %-10s ENDPOINT: %-20s" % (event.status, event.endpoint)
- if event.age: msg += " AGE: %-3s" % event.age
- if event.read_bytes: msg += " READ: %-4i" % event.read_bytes
- if event.wrote_bytes: msg += " WRITTEN: %-4i" % event.wrote_bytes
- if event.reason: msg += " REASON: %-6s" % event.reason
- if event.ncircs: msg += " NCIRCS: %i" % event.ncircs
- self._notify(event, msg)
-
- def stream_bw_event(self, event):
- self._notify(event, "ID: %s READ: %s WRITTEN: %s" % (event.strm_id, event.bytes_read, event.bytes_written))
-
- def bandwidth_event(self, event):
- self._notify(event, "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
-
- def msg_event(self, event):
- self._notify(event, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
-
- def new_desc_event(self, event):
- idlistStr = [str(item) for item in event.idlist]
- self._notify(event, ", ".join(idlistStr))
-
- def address_mapped_event(self, event):
- self._notify(event, "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr))
-
- def ns_event(self, event):
- # NetworkStatus params: nickname, idhash, orhash, ip, orport (int),
- # dirport (int), flags, idhex, bandwidth, updated (datetime)
- msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
- self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "blue")
-
- def new_consensus_event(self, event):
- msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
- self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
-
- def unknown_event(self, event):
- msg = "(%s) %s" % (event.event_name, event.event_string)
- self.callback(LogEntry(event.arrived_at, "UNKNOWN", msg, "red"))
-
- def _notify(self, event, msg, color="white"):
- self.callback(LogEntry(event.arrived_at, event.event_name, msg, color))
-
-class LogPanel(panel.Panel, threading.Thread):
- """
- Listens for and displays tor, arm, and torctl events. This can prepopulate
- from tor's log file if it exists.
- """
-
- def __init__(self, stdscr, loggedEvents, config=None):
- panel.Panel.__init__(self, stdscr, "log", 0)
- threading.Thread.__init__(self)
- self.setDaemon(True)
-
- self._config = dict(DEFAULT_CONFIG)
-
- if config:
- config.update(self._config, {
- "features.log.maxLinesPerEntry": 1,
- "features.log.prepopulateReadLimit": 0,
- "features.log.maxRefreshRate": 10,
- "cache.logPanel.size": 1000})
-
- # collapses duplicate log entries if false, showing only the most recent
- self.showDuplicates = self._config["features.log.showDuplicateEntries"]
-
- self.msgLog = [] # log entries, sorted by the timestamp
- self.loggedEvents = loggedEvents # events we're listening to
- 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._isPaused = False
- self._pauseBuffer = [] # location where messages are buffered if paused
-
- 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, _pauseBuffer
- 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)
-
- # fetches past tor events from log file, if available
- torEventBacklog = []
- if self._config["features.log.prepopulate"]:
- setRunlevels = list(set.intersection(set(self.loggedEvents), set(log.Runlevel.values())))
- readLimit = self._config["features.log.prepopulateReadLimit"]
- addLimit = self._config["cache.logPanel.size"]
- torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config)
-
- # adds arm listener and fetches past events
- log.LOG_LOCK.acquire()
- try:
- armRunlevels = [log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR]
- log.addListeners(armRunlevels, self._registerArmEvent)
-
- # gets the set of arm events we're logging
- setRunlevels = []
- for i in range(len(armRunlevels)):
- if "ARM_" + log.Runlevel.values()[i] in self.loggedEvents:
- setRunlevels.append(armRunlevels[i])
-
- armEventBacklog = []
- for level, msg, eventTime in log._getEntries(setRunlevels):
- armEventEntry = LogEntry(eventTime, "ARM_" + level, msg, RUNLEVEL_EVENT_COLOR[level])
- armEventBacklog.insert(0, armEventEntry)
-
- # joins armEventBacklog and torEventBacklog chronologically into msgLog
- while armEventBacklog or torEventBacklog:
- if not armEventBacklog:
- self.msgLog.append(torEventBacklog.pop(0))
- elif not torEventBacklog:
- self.msgLog.append(armEventBacklog.pop(0))
- elif armEventBacklog[0].timestamp < torEventBacklog[0].timestamp:
- self.msgLog.append(torEventBacklog.pop(0))
- else:
- self.msgLog.append(armEventBacklog.pop(0))
- finally:
- log.LOG_LOCK.release()
-
- # crops events that are either too old, or more numerous than the caching size
- self._trimEvents(self.msgLog)
-
- # leaving lastContentHeight as being too low causes initialization problems
- self.lastContentHeight = len(self.msgLog)
-
- # adds listeners for tor and torctl events
- conn = torTools.getConn()
- conn.addEventListener(TorEventObserver(self.registerEvent))
- conn.addTorCtlListener(self._registerTorCtlEvent)
-
- # opens log file if we'll be saving entries
- if self._config["features.logFile"]:
- logPath = self._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.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath))
- except (IOError, OSError), exc:
- log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
- self.logFile = None
-
- 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:
- self.logFile.write(event.getDisplayMessage(True) + "\n")
- self.logFile.flush()
- except IOError, exc:
- log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
- self.logFile = None
-
- if self._isPaused:
- self.valsLock.acquire()
- self._pauseBuffer.insert(0, event)
- self._trimEvents(self._pauseBuffer)
- self.valsLock.release()
- else:
- 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 _registerArmEvent(self, level, msg, eventTime):
- eventColor = RUNLEVEL_EVENT_COLOR[level]
- self.registerEvent(LogEntry(eventTime, "ARM_%s" % level, msg, eventColor))
-
- def _registerTorCtlEvent(self, level, msg):
- eventColor = RUNLEVEL_EVENT_COLOR[level]
- self.registerEvent(LogEntry(time.time(), "TORCTL_%s" % level, msg, eventColor))
-
- 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()
- self.loggedEvents = eventTypes
- self.redraw(True)
- self.valsLock.release()
-
- 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 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
- """
-
- # make dir if the path doesn't already exist
- baseDir = os.path.dirname(path)
- if not os.path.exists(baseDir): os.makedirs(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):
- 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
- self.redraw(True)
- self.valsLock.release()
- elif key in (ord('u'), ord('U')):
- self.valsLock.acquire()
- self.showDuplicates = not self.showDuplicates
- self.redraw(True)
- self.valsLock.release()
-
- def setPaused(self, isPause):
- """
- If true, prevents message log from being updated with new events.
- """
-
- if isPause == self._isPaused: return
-
- self._isPaused = isPause
- if self._isPaused: self._pauseBuffer = []
- else:
- self.valsLock.acquire()
- self.msgLog = (self._pauseBuffer + self.msgLog)[:self._config["cache.logPanel.size"]]
- self.redraw(True)
- self.valsLock.release()
-
- 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.
- """
-
- self.valsLock.acquire()
- self._lastLoggedEvents, self._lastUpdate = list(self.msgLog), time.time()
-
- # draws the top label
- 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 self._config["features.log.showDateDividers"]
- eventLog = getDaybreaks(self.msgLog, self._isPaused) if isDatesShown else list(self.msgLog)
- if not self.showDuplicates:
- deduplicatedLog = getDuplicates(eventLog)
-
- if deduplicatedLog == None:
- msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive."
- log.log(log.WARN, msg)
- self.showDuplicates = True
- 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
- if seenFirstDateDivider:
- if lineCount >= 1 and lineCount < height and showDaybreaks:
- self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr)
- self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
- self.addch(lineCount, width, 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) - 2
- 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 = self._config["features.log.maxLinesPerEntry"]
- while displayQueue:
- msg, format, includeBreak = displayQueue.pop(0)
- drawLine = lineCount + lineOffset
- if lineOffset == maxEntriesPerLine: break
-
- maxMsgSize = width - cursorLoc
- if len(msg) > maxMsgSize:
- # message is too long - break it up
- if lineOffset == maxEntriesPerLine - 1:
- msg = uiTools.cropStr(msg, maxMsgSize)
- 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, 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 - 1, dividerAttr)
- self.addch(lineCount, width, 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:
- forceRedrawReason = "scrolled off the bottom of the page"
- elif not isScrollBarVisible and newContentHeight > height - 1:
- forceRedrawReason = "scroll bar wasn't previously visible"
- elif isScrollBarVisible and newContentHeight <= height - 1:
- forceRedrawReason = "scroll bar shouldn't be visible"
- else: forceRedraw = False
-
- self.lastContentHeight = newContentHeight
- if forceRedraw:
- forceRedrawReason = "redrawing the log panel with the corrected content height (%s)" % forceRedrawReason
- log.log(self._config["log.logPanel.forceDoubleRedraw"], forceRedrawReason)
- 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 = self._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)
- self._cond.release()
- else:
- lastDay = currentDay
- self.redraw(True)
-
- def stop(self):
- """
- Halts further resolutions and terminates the thread.
- """
-
- self._cond.acquire()
- self._halt = True
- self._cond.notifyAll()
- self._cond.release()
-
- 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()
- currentPattern = self.regexFilter.pattern if self.regexFilter else None
- isUnchanged = self._titleArgs[0] == self.loggedEvents
- isUnchanged &= self._titleArgs[1] == currentPattern
- isUnchanged &= self._titleArgs[2] == width
- if isUnchanged:
- self.valsLock.release()
- return self._titleCache
-
- eventsList = list(self.loggedEvents)
- if not eventsList:
- if not currentPattern:
- panelLabel = "Events:"
- else:
- labelPattern = uiTools.cropStr(currentPattern, width - 18)
- panelLabel = "Events (filter: %s):" % labelPattern
- else:
- # does the following with all runlevel types (tor, arm, and torctl):
- # - pulls to the start of the list
- # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN")
- # - condense further if there's identical runlevel ranges for multiple
- # 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 = log.Runlevel.values()
- reversedRunlevels.reverse()
- for prefix in ("TORCTL_", "ARM_", ""):
- # blank ending runlevel forces the break condition to be reached at the end
- for runlevel in reversedRunlevels + [""]:
- eventType = prefix + runlevel
- if runlevel and eventType in eventsList:
- # runlevel event found, move to the tmp list
- eventsList.remove(eventType)
- tmpRunlevels.append(runlevel)
- elif tmpRunlevels:
- # adds all tmp list entries to the start of eventsList
- if len(tmpRunlevels) >= 3:
- # save condense sequential runlevels to be added later
- runlevelRanges.append((prefix, tmpRunlevels[-1], tmpRunlevels[0]))
- else:
- # 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 = self._config["cache.logPanel.size"]
- if len(eventListing) > cacheSize: del eventListing[cacheSize:]
-
- logTTL = self._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/src/interface/torrcPanel.py b/src/interface/torrcPanel.py
deleted file mode 100644
index b7cad86..0000000
--- a/src/interface/torrcPanel.py
+++ /dev/null
@@ -1,221 +0,0 @@
-"""
-Panel displaying the torrc or armrc with the validation done against it.
-"""
-
-import math
-import curses
-import threading
-
-from util import conf, enum, panel, torConfig, uiTools
-
-DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
- "features.config.file.maxLinesPerEntry": 8}
-
-# TODO: The armrc use case is incomplete. There should be equivilant reloading
-# and validation capabilities to the torrc.
-Config = enum.Enum("TORRC", "ARMRC") # configuration file types that can be displayed
-
-class TorrcPanel(panel.Panel):
- """
- Renders the current torrc or armrc with syntax highlighting in a scrollable
- area.
- """
-
- def __init__(self, stdscr, configType, config=None):
- panel.Panel.__init__(self, stdscr, "configFile", 0)
-
- self._config = dict(DEFAULT_CONFIG)
- if config:
- config.update(self._config, {"features.config.file.maxLinesPerEntry": 1})
-
- self.valsLock = threading.RLock()
- self.configType = configType
- self.scroll = 0
- self.showLabel = True # shows top label (hides otherwise)
- self.showLineNum = True # shows left aligned line numbers
- self.stripComments = False # drops comments and extra whitespace
-
- # height of the content when last rendered (the cached value is invalid if
- # _lastContentHeightArgs is None or differs from the current dimensions)
- self._lastContentHeight = 1
- self._lastContentHeightArgs = None
-
- def handleKey(self, key):
- self.valsLock.acquire()
- if uiTools.isScrollKey(key):
- pageHeight = self.getPreferredSize()[0] - 1
- newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
-
- if self.scroll != newScroll:
- self.scroll = newScroll
- self.redraw(True)
- elif key == ord('n') or key == ord('N'):
- self.showLineNum = not self.showLineNum
- self._lastContentHeightArgs = None
- self.redraw(True)
- elif key == ord('s') or key == ord('S'):
- self.stripComments = not self.stripComments
- self._lastContentHeightArgs = None
- self.redraw(True)
-
- self.valsLock.release()
-
- def draw(self, 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.getConfig("arm")
- confLocation = loadedArmrc.path
- renderedContents = list(loadedArmrc.rawContents)
-
- # offset to make room for the line numbers
- lineNumOffset = 0
- if self.showLineNum:
- if len(renderedContents) == 0: lineNumOffset = 2
- else: lineNumOffset = int(math.log10(len(renderedContents))) + 2
-
- # draws left-hand scroll bar if content's longer than the height
- scrollOffset = 0
- if self._config["features.config.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.showLabel:
- 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(" ")
- if isMultiline:
- # part of a multiline entry started on a previous line so everything
- # is part of the argument
- lineComp["argument"][0] = lineText
- elif optionIndex == -1:
- # no argument provided
- lineComp["option"][0] = lineText
- else:
- optionText = strippedLine[:optionIndex]
- 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")
- elif lineIssue == torConfig.ValidationError.MISMATCH:
- lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red")
- lineComp["correction"][0] = " (%s)" % lineIssueMsg
- else:
- # For some types of configs the correction field is simply used to
- # provide extra data (for instance, the type for tor state fields).
- lineComp["correction"][0] = " (%s)" % lineIssueMsg
- lineComp["correction"][1] = curses.A_BOLD | uiTools.getColor("magenta")
-
- # draws the line number
- if self.showLineNum and displayLine < height and displayLine >= 1:
- lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
- self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
-
- # draws the rest of the components with line wrap
- cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
- maxLinesPerEntry = self._config["features.config.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
- if lineOffset == maxLinesPerEntry - 1:
- msg = uiTools.cropStr(msg, maxMsgSize)
- else:
- 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/src/starter.py b/src/starter.py
index 09fc37a..c0a0270 100644
--- a/src/starter.py
+++ b/src/starter.py
@@ -14,8 +14,8 @@ import socket
import platform
import version
-import interface.controller
-import interface.logPanel
+import cli.controller
+import cli.logPanel
import util.conf
import util.connections
import util.hostnames
@@ -67,7 +67,7 @@ Terminal status monitor for Tor relays.
Example:
arm -b -i 1643 hide connection data, attaching to control port 1643
arm -e we -c /tmp/cfg use this configuration file with 'WARN'/'ERR' events
-""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], interface.logPanel.EVENT_LISTING)
+""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], cli.logPanel.EVENT_LISTING)
# filename used for cached tor config descriptions
CONFIG_DESC_FILENAME = "torConfigDesc.txt"
@@ -312,7 +312,7 @@ if __name__ == '__main__':
# validates and expands log event flags
try:
- expandedEvents = interface.logPanel.expandEvents(param["startup.events"])
+ expandedEvents = cli.logPanel.expandEvents(param["startup.events"])
except ValueError, exc:
for flag in str(exc):
print "Unrecognized event flag: %s" % flag
@@ -387,5 +387,5 @@ if __name__ == '__main__':
util.log.log(CONFIG["log.savingDebugLog"], "Saving a debug log to '%s' (please check it for sensitive information before sharing)" % LOG_DUMP_PATH)
_dumpConfig()
- interface.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"])
+ cli.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"])
1
0

17 Jul '11
commit 1161a6a6e72dff62fe019b9d15cc0943958359f7
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Apr 25 19:13:43 2011 -0700
Dropping locales when geoip db is unavailable
Previously the connection panel showed '??' for all locales when the geoip
database was unavailable. Dropping these entries from the interface entirely
instead.
---
src/interface/connections/connEntry.py | 3 ++-
src/util/torTools.py | 14 +++++++++++---
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/src/interface/connections/connEntry.py b/src/interface/connections/connEntry.py
index f613fc2..e6c0d92 100644
--- a/src/interface/connections/connEntry.py
+++ b/src/interface/connections/connEntry.py
@@ -824,8 +824,9 @@ class ConnectionLine(entries.ConnectionPanelLine):
dstAddress += " (%s)" % purpose
elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
extraInfo = []
+ conn = torTools.getConn()
- if includeLocale:
+ if includeLocale and not conn.isGeoipUnavailable():
foreignLocale = self.foreign.getLocale("??")
extraInfo.append(foreignLocale)
spaceAvailable -= len(foreignLocale) + 2
diff --git a/src/util/torTools.py b/src/util/torTools.py
index 31a7061..0eebd20 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -474,19 +474,19 @@ class Controller(TorCtl.PostEventListener):
if isCacheArg and cachedValue:
result = cachedValue
isFromCache = True
- elif isGeoipRequest and self.geoipFailureCount == GEOIP_FAILURE_THRESHOLD:
+ elif isGeoipRequest and self.isGeoipUnavailable():
# the geoip database aleady looks to be unavailable - abort the request
raisedExc = TorCtl.ErrorReply("Tor geoip database is unavailable.")
else:
try:
getInfoVal = self.conn.get_info(param)[param]
if getInfoVal != None: result = getInfoVal
- if isGeoipRequest: self.geoipFailureCount = 0
+ if isGeoipRequest: self.geoipFailureCount = -1
except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
if type(exc) == TorCtl.TorCtlClosed: self.close()
raisedExc = exc
- if isGeoipRequest:
+ if isGeoipRequest and not self.geoipFailureCount == -1:
self.geoipFailureCount += 1
if self.geoipFailureCount == GEOIP_FAILURE_THRESHOLD:
@@ -833,6 +833,14 @@ class Controller(TorCtl.PostEventListener):
return result
+ def isGeoipUnavailable(self):
+ """
+ Provides true if we've concluded that our geoip database is unavailable,
+ false otherwise.
+ """
+
+ return self.geoipFailureCount == GEOIP_FAILURE_THRESHOLD
+
def getMyPid(self):
"""
Provides the pid of the attached tor process (None if no controller exists
1
0

17 Jul '11
commit e4bf17a30fbeaa24f6980476ceb8e1a51ad0ba62
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Apr 26 08:55:19 2011 -0700
fix: minor commenting correction on lsof results
A previous commit noted that lsof queries return itself in the results, so
correcting the comment on this.
---
src/util/torTools.py | 4 ++--
1 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/util/torTools.py b/src/util/torTools.py
index f4a29af..5d8ffd8 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -225,8 +225,8 @@ def getPid(controlPort=9051, pidFilePath=None):
try:
results = sysTools.call("lsof -wnPi | egrep \"^tor.*:%i\"" % controlPort)
- # This can result in multiple entries with the same pid (maybe from the
- # query itself?). Checking all lines to see if they have the same pid.
+ # This can result in multiple entries with the same pid (from the query
+ # itself). Checking all lines to see if they're in agreement about the pid.
if results:
pid = ""
1
0

17 Jul '11
commit 9b653e81ba24b154510e8fc39222de5c96478bc2
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Apr 24 12:21:34 2011 -0700
fix: Workaround for autogenerated Nickname config
As discussed in ticket 2362
(https://trac.torproject.org/projects/tor/ticket/2362) the "GETINFO
config-text" results provide an autogenerated Nickname field if running as a
relay and one wasn't provided.
---
src/util/torConfig.py | 8 ++++++--
1 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/util/torConfig.py b/src/util/torConfig.py
index 2e70f5a..030a169 100644
--- a/src/util/torConfig.py
+++ b/src/util/torConfig.py
@@ -3,6 +3,7 @@ Helper functions for working with tor's configuration file.
"""
import os
+import socket
import threading
from util import enum, log, sysTools, torTools, uiTools
@@ -367,13 +368,16 @@ def getCustomOptions(includeValue = False):
configLines = list(set(configLines))
# The "GETINFO config-text" query only provides options that differ
- # from Tor's defaults with the exception of its Log entry which, even
- # if undefined, returns "Log notice stdout" as per:
+ # from Tor's defaults with the exception of its Log and Nickname entries
+ # which, even if undefined, returns "Log notice stdout" as per:
# https://trac.torproject.org/projects/tor/ticket/2362
try: configLines.remove("Log notice stdout")
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]
1
0

17 Jul '11
commit cd6a6873e753fa4497250ab1c5c332f3e6fe86cf
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Apr 25 19:21:56 2011 -0700
Making lsof the sole connection resolver on macs
On macs connection resolver detection from the path isn't working, and
aparently only lsof is available. This results in us trying (and failing) all
of the other resolvers before falling back to something that works. This drops
us straight to using lsof on macs.
---
src/util/connections.py | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
diff --git a/src/util/connections.py b/src/util/connections.py
index a797e7b..f632306 100644
--- a/src/util/connections.py
+++ b/src/util/connections.py
@@ -337,6 +337,8 @@ def getSystemResolvers(osType = None):
if osType == "FreeBSD":
resolvers = [Resolver.BSD_SOCKSTAT, Resolver.BSD_PROCSTAT, Resolver.LSOF]
+ elif osType == "Darwin":
+ resolvers = [Resolver.LSOF]
else:
resolvers = [Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS]
1
0

17 Jul '11
commit 36512bdc7608db5f225e58926ac0b5f383f6b026
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Apr 25 18:51:13 2011 -0700
fix: Missing configuration key for missing geoip
This causes a crashing error when the Tor geoip file is unavailable, and
locales are queried.
---
src/util/torTools.py | 3 ++-
1 files changed, 2 insertions(+), 1 deletions(-)
diff --git a/src/util/torTools.py b/src/util/torTools.py
index b5913ed..31a7061 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -82,7 +82,8 @@ CONFIG = {"torrc.map": {},
"log.torSetConf": log.INFO,
"log.torPrefixPathInvalid": log.NOTICE,
"log.bsdJailFound": log.INFO,
- "log.unknownBsdJailId": log.WARN}
+ "log.unknownBsdJailId": log.WARN,
+ "log.geoipUnavailable": log.WARN}
# events used for controller functionality:
# NOTICE - used to detect when tor is shut down
1
0

17 Jul '11
commit c62e15023f3f6fe791a5f6fc21f070c6e8f857fb
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Apr 25 19:52:07 2011 -0700
PID resolution fallbacks by ps and lsof
On Macs all of the current pid resolution tactics fails, so adding a couple
more that work on that platform.
---
src/util/torTools.py | 28 ++++++++++++++++++++++++++++
1 files changed, 28 insertions(+), 0 deletions(-)
diff --git a/src/util/torTools.py b/src/util/torTools.py
index 0eebd20..08b04c0 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -122,6 +122,8 @@ def getPid(controlPort=9051, pidFilePath=None):
4. "netstat -npl | grep 127.0.0.1:%s" % <tor control port>
5. "ps -o pid -C tor"
6. "sockstat -4l -P tcp -p %i | grep tor" % <tor control port>
+ 7. "ps axc | egrep \" tor$\""
+ 8. "lsof -wnPi | egrep \"^tor.*:%i\"" % <tor control port>
If pidof or ps provide multiple tor instances then their results are
discarded (since only netstat can differentiate using the control port). This
@@ -201,6 +203,32 @@ def getPid(controlPort=9051, pidFilePath=None):
if pid.isdigit(): return pid
except IOError: pass
+ # attempts to resolve via a ps command that works on the mac (this and lsof
+ # are the only resolvers to work on that platform). This fails if:
+ # - tor's running under a different name
+ # - there's multiple instances of tor
+
+ try:
+ results = sysTools.call("ps axc | egrep \" tor$\"")
+ if len(results) == 1 and len(results[0].split()) > 0:
+ pid = results[0].split()[0]
+ if pid.isdigit(): return pid
+ except IOError: pass
+
+ # attempts to resolve via lsof - this should work on linux, mac, and bsd -
+ # this fails if:
+ # - tor's running under a different name
+ # - tor's being run as a different user due to permissions
+ # - there are multiple instances of Tor, using the
+ # same control port on different addresses.
+
+ try:
+ results = sysTools.call("lsof -wnPi | egrep \"^tor.*:%i\"" % controlPort)
+ if len(results) == 1 and len(results[0].split()) > 1:
+ pid = results[0].split()[1]
+ if pid.isdigit(): return pid
+ except IOError: pass
+
return None
def getBsdJailId():
1
0

17 Jul '11
commit 12077adddb88f1a2e7ae2f8e0078c54ce3e70fd2
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Apr 26 08:32:50 2011 -0700
fix: Accounting for multiple results from lsof
When querying lsof from the python process this can provide multiple results
with the same pid. The second is from the lsof query itself (this was observed
with an earlier change for querying our own process). Accounting for this by
checking all pid results from the query to see if they're in agreement.
---
src/util/torTools.py | 16 ++++++++++++++--
1 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/src/util/torTools.py b/src/util/torTools.py
index 08b04c0..f4a29af 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -224,8 +224,20 @@ def getPid(controlPort=9051, pidFilePath=None):
try:
results = sysTools.call("lsof -wnPi | egrep \"^tor.*:%i\"" % controlPort)
- if len(results) == 1 and len(results[0].split()) > 1:
- pid = results[0].split()[1]
+
+ # This can result in multiple entries with the same pid (maybe from the
+ # query itself?). Checking all lines to see if they have the same pid.
+
+ if results:
+ pid = ""
+
+ for line in results:
+ lineComp = line.split()
+
+ if len(lineComp) >= 2 and (not pid or lineComp[1] == pid):
+ pid = lineComp[1]
+ else: raise IOError
+
if pid.isdigit(): return pid
except IOError: pass
1
0

17 Jul '11
commit b4ea3bd612d54ff1e33c4b52d4982ccfc207d9fb
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Apr 23 20:34:31 2011 -0700
Separating inital BW totals into separate params
Mixing the bandwidth totals fetched from Tor with the GraphStats param caused
the average measurement to be skewed. Separating this total into a parameter
just used for the total label.
---
armrc.sample | 4 ----
src/interface/graphing/bandwidthStats.py | 14 +++++++-------
2 files changed, 7 insertions(+), 11 deletions(-)
diff --git a/armrc.sample b/armrc.sample
index 11b62ea..4cda197 100644
--- a/armrc.sample
+++ b/armrc.sample
@@ -140,9 +140,6 @@ features.graph.showIntermediateBounds true
# prepopulate
# attempts to use tor's state file to prepopulate the bandwidth graph at the
# 15-minute interval (this requires the minimum of a day's worth of uptime)
-# prepopulateTotal
-# populates the total stat from the state file if true (this only contains
-# the last day's worth of information, so this metric isn't the true total)
# transferInBystes
# shows rate measurments in bytes if true, bits otherwise
# accounting.show
@@ -153,7 +150,6 @@ features.graph.showIntermediateBounds true
# provides verbose measurements of time if true
features.graph.bw.prepopulate true
-features.graph.bw.prepopulateTotal false
features.graph.bw.transferInBytes false
features.graph.bw.accounting.show true
features.graph.bw.accounting.rate 10
diff --git a/src/interface/graphing/bandwidthStats.py b/src/interface/graphing/bandwidthStats.py
index 70a1e16..f8e3020 100644
--- a/src/interface/graphing/bandwidthStats.py
+++ b/src/interface/graphing/bandwidthStats.py
@@ -60,19 +60,18 @@ class BandwidthStats(graphPanel.GraphStats):
# https://trac.torproject.org/projects/tor/ticket/2345
#
# further updates are still handled via BW events to avoid unnecessary
- # GETINFO requests. This needs to update the pause buffer too because
- # instances start paused, causing the primary value to be clobbered once
- # just after initalization.
+ # GETINFO requests.
+
+ self.initialPrimaryTotal = 0
+ self.initialSecondaryTotal = 0
readTotal = conn.getInfo("traffic/read")
if readTotal and readTotal.isdigit():
- self.primaryTotal = int(readTotal) / 1024 # Bytes -> KB
- self._pauseBuffer.primaryTotal = int(readTotal) / 1024
+ self.initialPrimaryTotal = int(readTotal) / 1024 # Bytes -> KB
writeTotal = conn.getInfo("traffic/written")
if writeTotal and writeTotal.isdigit():
- self.secondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
- self._pauseBuffer.secondaryTotal = int(writeTotal) / 1024
+ self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
def resetListener(self, conn, eventType):
# updates title parameters and accounting status if they changed
@@ -346,6 +345,7 @@ class BandwidthStats(graphPanel.GraphStats):
def _getTotalLabel(self, isPrimary):
total = self.primaryTotal if isPrimary else self.secondaryTotal
+ total += self.initialPrimaryTotal if isPrimary else self.initialSecondaryTotal
return "total: %s" % uiTools.getSizeLabel(total * 1024, 1)
def _updateAccountingInfo(self):
1
0

17 Jul '11
commit 301085e92801173d884e8a1282539282331244d8
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Apr 23 20:24:55 2011 -0700
Initializing bandwidth totals from Tor when able
Ticket 2345 (https://trac.torproject.org/projects/tor/ticket/2345) introduced
GETINFO options for the total read and written bandwidth values. If these are
available then they're used to populate the total values in arm.
---
src/interface/graphing/bandwidthStats.py | 19 +++++++++++++++++++
src/settings.cfg | 4 ++++
2 files changed, 23 insertions(+), 0 deletions(-)
diff --git a/src/interface/graphing/bandwidthStats.py b/src/interface/graphing/bandwidthStats.py
index 1955465..3f34e3c 100644
--- a/src/interface/graphing/bandwidthStats.py
+++ b/src/interface/graphing/bandwidthStats.py
@@ -55,6 +55,25 @@ class BandwidthStats(graphPanel.GraphStats):
self._titleStats, self.isAccounting = [], False
self.resetListener(conn, torTools.State.INIT) # 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. This needs to update the pause buffer too because
+ # instances start paused, causing the primary value to be clobbered once
+ # just after initalization.
+
+ readTotal = conn.getInfo("traffic/read")
+ if readTotal and readTotal.isdigit():
+ self.primaryTotal = int(readTotal) / 1024 # Bytes -> KB
+ self._pauseBuffer.primaryTotal = int(readTotal) / 1024
+
+ writeTotal = conn.getInfo("traffic/written")
+ if writeTotal and writeTotal.isdigit():
+ self.secondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
+ self._pauseBuffer.secondaryTotal = int(writeTotal) / 1024
def resetListener(self, conn, eventType):
# updates title parameters and accounting status if they changed
diff --git a/src/settings.cfg b/src/settings.cfg
index 0e0e395..1ee63a1 100644
--- a/src/settings.cfg
+++ b/src/settings.cfg
@@ -273,6 +273,8 @@ config.summary.TestingEstimatedDescriptorPropagationTime delay before clients at
# [ARM_DEBUG] recreating panel 'graph' with the dimensions of 14/124
# [ARM_DEBUG] redrawing the log panel with the corrected content height (estimat was off by 4)
# [ARM_DEBUG] GETINFO accounting/bytes-left (runtime: 0.0006)
+# [ARM_DEBUG] GETINFO traffic/read (runtime: 0.0004)
+# [ARM_DEBUG] GETINFO traffic/written (runtime: 0.0002)
# [ARM_DEBUG] GETCONF MyFamily (runtime: 0.0007)
# [ARM_DEBUG] Unable to query process resource usage from ps, waiting 6.25 seconds (unrecognized output from ps: ...)
@@ -311,6 +313,8 @@ msg.ARM_DEBUG GETINFO accounting/bytes
msg.ARM_DEBUG GETINFO accounting/bytes-left
msg.ARM_DEBUG GETINFO accounting/interval-end
msg.ARM_DEBUG GETINFO accounting/hibernating
+msg.ARM_DEBUG GETINFO traffic/read
+msg.ARM_DEBUG GETINFO traffic/written
msg.ARM_DEBUG GETCONF
msg.ARM_DEBUG Unable to query process resource usage from ps
1
0