[or-cvs] r23010: {arm} added: setup script for installing to '/usr/local/arm'. This (in arm/trunk: . src src/interface src/interface/graphing src/util)
Damian Johnson
atagar1 at gmail.com
Sat Aug 21 20:38:47 UTC 2010
Author: atagar
Date: 2010-08-21 20:38:47 +0000 (Sat, 21 Aug 2010)
New Revision: 23010
Added:
arm/trunk/setup.cfg
arm/trunk/setup.py
arm/trunk/src/
arm/trunk/src/TorCtl/
arm/trunk/src/__init__.py
arm/trunk/src/interface/
arm/trunk/src/interface/confPanel.py
arm/trunk/src/interface/controller.py
arm/trunk/src/interface/graphing/bandwidthStats.py
arm/trunk/src/interface/graphing/graphPanel.py
arm/trunk/src/interface/graphing/psStats.py
arm/trunk/src/interface/logPanel.py
arm/trunk/src/prereq.py
arm/trunk/src/starter.py
arm/trunk/src/util/
arm/trunk/src/util/torTools.py
Removed:
arm/trunk/init/
arm/trunk/interface/
arm/trunk/src/interface/confPanel.py
arm/trunk/src/interface/controller.py
arm/trunk/src/interface/graphing/bandwidthStats.py
arm/trunk/src/interface/graphing/graphPanel.py
arm/trunk/src/interface/graphing/psStats.py
arm/trunk/src/interface/logPanel.py
arm/trunk/src/util/torTools.py
arm/trunk/util/
Modified:
arm/trunk/README
arm/trunk/arm
Log:
added: setup script for installing to '/usr/local/arm'. This is not yet functional since it doesn't add a '/usr/local/bin' entry yet.
- changing the layout to accomidate distutils
- removing the consensus tracker (this will be added to the tor tools when the following is resolved:
https://trac.torproject.org/projects/tor/ticket/1737
Modified: arm/trunk/README
===================================================================
--- arm/trunk/README 2010-08-21 15:33:23 UTC (rev 23009)
+++ arm/trunk/README 2010-08-21 20:38:47 UTC (rev 23010)
@@ -94,39 +94,39 @@
README - um... guess you figured this one out
TODO - known issues, future plans, etc
- init/
+ src/arm/
__init__.py
starter.py - parses and validates commandline parameters
prereq.py - checks python version and for required packages
-
- interface/
- graphing/
+
+ interface/
+ graphing/
+ __init__.py
+ graphPanel.py - (page 1) presents graphs for data instances
+ bandwidthStats.py - tracks tor bandwidth usage
+ psStats.py - tracks system information (such as cpu/memory usage)
+ connStats.py - tracks number of tor connections
+
__init__.py
- graphPanel.py - (page 1) presents graphs for data instances
- bandwidthStats.py - tracks tor bandwidth usage
- psStats.py - tracks system information (such as cpu/memory usage)
- connStats.py - tracks number of tor connections
+ controller.py - main display loop, handling input and layout
+ headerPanel.py - top of all pages, providing general information
+
+ logPanel.py - (page 1) displays tor, arm, and torctl events
+ fileDescriptorPopup.py - (popup) displays file descriptors used by tor
+
+ connPanel.py - (page 2) displays information on tor connections
+ descriptorPopup.py - (popup) displays connection descriptor data
+
+ confPanel.py - (page 3) displays torrc and performs validation
- __init__.py
- controller.py - main display loop, handling input and layout
- headerPanel.py - top of all pages, providing general information
-
- logPanel.py - (page 1) displays tor, arm, and torctl events
- fileDescriptorPopup.py - (popup) displays file descriptors used by tor
-
- connPanel.py - (page 2) displays information on tor connections
- descriptorPopup.py - (popup) displays connection descriptor data
-
- confPanel.py - (page 3) displays torrc and performs validation
-
- util/
- __init__.py
- conf.py - loading and persistence for user configuration
- connections.py - service providing periodic connection lookups
- hostnames.py - service providing nonblocking reverse dns lookups
- log.py - aggregator for application events
- panel.py - wrapper for safely working with curses subwindows
- sysTools.py - helper for system calls, providing client side caching
- torTools.py - TorCtl wrapper, providing caching and derived information
- uiTools.py - helper functions for presenting the user interface
+ util/
+ __init__.py
+ conf.py - loading and persistence for user configuration
+ connections.py - service providing periodic connection lookups
+ hostnames.py - service providing nonblocking reverse dns lookups
+ log.py - aggregator for application events
+ panel.py - wrapper for safely working with curses subwindows
+ sysTools.py - helper for system calls, providing client side caching
+ torTools.py - TorCtl wrapper, providing caching and derived information
+ uiTools.py - helper functions for presenting the user interface
Modified: arm/trunk/arm
===================================================================
--- arm/trunk/arm 2010-08-21 15:33:23 UTC (rev 23009)
+++ arm/trunk/arm 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,8 +1,8 @@
#!/bin/sh
-python init/prereq.py
+python src/arm/prereq.py
if [ $? = 0 ]
then
- python -W ignore::DeprecationWarning init/starter.py $*
+ python -W ignore::DeprecationWarning src/arm/starter.py $*
fi
Added: arm/trunk/setup.cfg
===================================================================
--- arm/trunk/setup.cfg (rev 0)
+++ arm/trunk/setup.cfg 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,2 @@
+[install]
+install-purelib=/usr/local
Added: arm/trunk/setup.py
===================================================================
--- arm/trunk/setup.py (rev 0)
+++ arm/trunk/setup.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+
+from distutils.core import setup
+
+setup(name='arm',
+ version='1.3.6_dev',
+ description='Terminal tor status monitor',
+ license='GPL v3',
+ author='Damian Johnson',
+ author_email='atagar at torproject.org',
+ url='http://www.atagar.com/arm/',
+ packages=['arm', 'arm.interface', 'arm.util', 'arm.TorCtl'],
+ package_dir={'arm': 'src'},
+ )
+
Added: arm/trunk/src/__init__.py
===================================================================
--- arm/trunk/src/__init__.py (rev 0)
+++ arm/trunk/src/__init__.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,6 @@
+"""
+Scripts involved in validating user input, system state, and initializing arm.
+"""
+
+__all__ = ["starter", "prereq"]
+
Deleted: arm/trunk/src/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py 2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/confPanel.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,292 +0,0 @@
-#!/usr/bin/env python
-# confPanel.py -- Presents torrc with syntax highlighting.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import math
-import curses
-import socket
-
-import controller
-from TorCtl import TorCtl
-from util import log, panel, uiTools
-
-# torrc parameters that can be defined multiple times without overwriting
-# from src/or/config.c (entries with LINELIST or LINELIST_S)
-# last updated for tor version 0.2.1.19
-MULTI_LINE_PARAM = ["AlternateBridgeAuthority", "AlternateDirAuthority", "AlternateHSAuthority", "AuthDirBadDir", "AuthDirBadExit", "AuthDirInvalid", "AuthDirReject", "Bridge", "ControlListenAddress", "ControlSocket", "DirListenAddress", "DirPolicy", "DirServer", "DNSListenAddress", "ExitPolicy", "HashedControlPassword", "HiddenServiceDir", "HiddenServiceOptions", "HiddenServicePort", "HiddenServiceVersion", "HiddenServiceAuthorizeClient", "HidServAuth", "Log", "MapAddress", "NatdListenAddress", "NodeFamily", "ORListenAddress", "ReachableAddresses", "ReachableDirAddresses", "ReachableORAddresses", "RecommendedVersions", "RecommendedClientVersions", "RecommendedServerVersions", "SocksListenAddress", "SocksPolicy", "TransListenAddress", "__HashedControlSessionPassword"]
-
-# hidden service options need to be fetched with HiddenServiceOptions
-HIDDEN_SERVICE_PARAM = ["HiddenServiceDir", "HiddenServiceOptions", "HiddenServicePort", "HiddenServiceVersion", "HiddenServiceAuthorizeClient"]
-HIDDEN_SERVICE_FETCH_PARAM = "HiddenServiceOptions"
-
-# size modifiers allowed by config.c
-LABEL_KB = ["kb", "kbyte", "kbytes", "kilobyte", "kilobytes"]
-LABEL_MB = ["m", "mb", "mbyte", "mbytes", "megabyte", "megabytes"]
-LABEL_GB = ["gb", "gbyte", "gbytes", "gigabyte", "gigabytes"]
-LABEL_TB = ["tb", "terabyte", "terabytes"]
-
-# GETCONF aliases (from the _option_abbrevs struct of src/or/config.c)
-# fix for: https://trac.torproject.org/projects/tor/ticket/1798
-# TODO: remove if/when fixed in tor
-CONF_ALIASES = {"l": "Log",
- "AllowUnverifiedNodes": "AllowInvalidNodes",
- "AutomapHostSuffixes": "AutomapHostsSuffixes",
- "AutomapHostOnResolve": "AutomapHostsOnResolve",
- "BandwidthRateBytes": "BandwidthRate",
- "BandwidthBurstBytes": "BandwidthBurst",
- "DirFetchPostPeriod": "StatusFetchPeriod",
- "MaxConn": "ConnLimit",
- "ORBindAddress": "ORListenAddress",
- "DirBindAddress": "DirListenAddress",
- "SocksBindAddress": "SocksListenAddress",
- "UseHelperNodes": "UseEntryGuards",
- "NumHelperNodes": "NumEntryGuards",
- "UseEntryNodes": "UseEntryGuards",
- "NumEntryNodes": "NumEntryGuards",
- "ResolvConf": "ServerDNSResolvConfFile",
- "SearchDomains": "ServerDNSSearchDomains",
- "ServerDNSAllowBrokenResolvConf": "ServerDNSAllowBrokenConfig",
- "PreferTunnelledDirConns": "PreferTunneledDirConns",
- "BridgeAuthoritativeDirectory": "BridgeAuthoritativeDir",
- "HashedControlPassword": "__HashedControlSessionPassword",
- "StrictEntryNodes": "StrictNodes",
- "StrictExitNodes": "StrictNodes"}
-
-
-# time modifiers allowed by config.c
-LABEL_MIN = ["minute", "minutes"]
-LABEL_HOUR = ["hour", "hours"]
-LABEL_DAY = ["day", "days"]
-LABEL_WEEK = ["week", "weeks"]
-
-class ConfPanel(panel.Panel):
- """
- Presents torrc with syntax highlighting in a scroll-able area.
- """
-
- def __init__(self, stdscr, confLocation, conn):
- panel.Panel.__init__(self, stdscr, "conf", 0)
- self.confLocation = confLocation
- self.showLineNum = True
- self.stripComments = False
- self.confContents = []
- self.scroll = 0
-
- # lines that don't matter due to duplicates
- self.irrelevantLines = []
-
- # used to check consistency with tor's actual values - corrections mapping
- # is of line numbers (one-indexed) to tor's actual values
- self.corrections = {}
- self.conn = conn
-
- self.reset()
-
- def reset(self, logErrors=True):
- """
- Reloads torrc contents and resets scroll height. Returns True if
- successful, else false.
- """
-
- try:
- resetSuccessful = True
-
- confFile = open(self.confLocation, "r")
- self.confContents = confFile.readlines()
- confFile.close()
-
- # checks if torrc differs from get_option data
- self.irrelevantLines = []
- self.corrections = {}
- parsedCommands = {} # mapping of parsed commands to line numbers
-
- for lineNumber in range(len(self.confContents)):
- lineText = self.confContents[lineNumber].strip()
-
- if lineText and lineText[0] != "#":
- # relevant to tor (not blank nor comment)
- ctlEnd = lineText.find(" ") # end of command
- argEnd = lineText.find("#") # end of argument (start of comment or end of line)
- if argEnd == -1: argEnd = len(lineText)
- command, argument = lineText[:ctlEnd], lineText[ctlEnd:argEnd].strip()
-
- # replace aliases with the internal representation of the command
- if command in CONF_ALIASES: command = CONF_ALIASES[command]
-
- # tor appears to replace tabs with a space, for instance:
- # "accept\t*:563" is read back as "accept *:563"
- argument = argument.replace("\t", " ")
-
- # expands value if it's a size or time
- comp = argument.strip().lower().split(" ")
- if len(comp) > 1:
- size = 0
- if comp[1] in LABEL_KB: size = int(comp[0]) * 1024
- elif comp[1] in LABEL_MB: size = int(comp[0]) * 1048576
- elif comp[1] in LABEL_GB: size = int(comp[0]) * 1073741824
- elif comp[1] in LABEL_TB: size = int(comp[0]) * 1099511627776
- elif comp[1] in LABEL_MIN: size = int(comp[0]) * 60
- elif comp[1] in LABEL_HOUR: size = int(comp[0]) * 3600
- elif comp[1] in LABEL_DAY: size = int(comp[0]) * 86400
- elif comp[1] in LABEL_WEEK: size = int(comp[0]) * 604800
- if size != 0: argument = str(size)
-
- # most parameters are overwritten if defined multiple times, if so
- # it's erased from corrections and noted as duplicate instead
- if not command in MULTI_LINE_PARAM and command in parsedCommands.keys():
- previousLineNum = parsedCommands[command]
- self.irrelevantLines.append(previousLineNum)
- if previousLineNum in self.corrections.keys(): del self.corrections[previousLineNum]
-
- parsedCommands[command] = lineNumber + 1
-
- # check validity against tor's actual state
- try:
- actualValues = []
- if command in HIDDEN_SERVICE_PARAM:
- # hidden services are fetched via a special command
- hsInfo = self.conn.get_option(HIDDEN_SERVICE_FETCH_PARAM)
- for entry in hsInfo:
- if entry[0] == command:
- actualValues.append(entry[1])
- break
- else:
- # general case - fetch all valid values
- for key, val in self.conn.get_option(command):
- if val == None:
- # TODO: investigate situations where this might occure
- # (happens if trying to parse HIDDEN_SERVICE_PARAM)
- if logErrors: log.log(log.WARN, "BUG: Failed to find torrc value for %s" % key)
- continue
-
- # TODO: check for a better way of figuring out CSV parameters
- # (kinda doubt this is right... in config.c its listed as being
- # a 'LINELIST') - still, good enough for common cases
- if command in MULTI_LINE_PARAM: toAdd = val.split(",")
- else: toAdd = [val]
-
- for newVal in toAdd:
- newVal = newVal.strip()
- if newVal not in actualValues: actualValues.append(newVal)
-
- # there might be multiple values on a single line - if so, check each
- if command in MULTI_LINE_PARAM and "," in argument:
- arguments = []
- for entry in argument.split(","):
- arguments.append(entry.strip())
- else:
- arguments = [argument]
-
- for entry in arguments:
- if not entry in actualValues:
- self.corrections[lineNumber + 1] = ", ".join(actualValues)
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
- if logErrors: log.log(log.WARN, "Unable to validate line %i of the torrc: %s" % (lineNumber + 1, lineText))
-
- # logs issues that arose
- if self.irrelevantLines and logErrors:
- if len(self.irrelevantLines) > 1: first, second, third = "Entries", "are", ", including lines"
- else: first, second, third = "Entry", "is", " on line"
- baseMsg = "%s in your torrc %s ignored due to duplication%s" % (first, second, third)
-
- log.log(log.NOTICE, "%s: %s (highlighted in blue)" % (baseMsg, ", ".join([str(val) for val in self.irrelevantLines])))
-
- if self.corrections and logErrors:
- log.log(log.WARN, "Tor's state differs from loaded torrc")
- except IOError, exc:
- resetSuccessful = False
- self.confContents = ["### Unable to load torrc ###"]
- if logErrors: log.log(log.WARN, "Unable to load torrc (%s)" % str(exc))
-
- self.scroll = 0
- return resetSuccessful
-
- def handleKey(self, key):
- pageHeight = self.getPreferredSize()[0] - 1
- 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.confContents) - pageHeight))
- elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - pageHeight, 0)
- elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + pageHeight, len(self.confContents) - pageHeight))
- elif key == ord('n') or key == ord('N'): self.showLineNum = not self.showLineNum
- elif key == ord('s') or key == ord('S'):
- self.stripComments = not self.stripComments
- self.scroll = 0
- self.redraw(True)
-
- def draw(self, subwindow, width, height):
- self.addstr(0, 0, "Tor Config (%s):" % self.confLocation, curses.A_STANDOUT)
-
- pageHeight = height - 1
- if self.confContents: numFieldWidth = int(math.log10(len(self.confContents))) + 1
- else: numFieldWidth = 0 # torrc is blank
- lineNum, displayLineNum = self.scroll + 1, 1 # lineNum corresponds to torrc, displayLineNum concerns what's presented
-
- # determine the ending line in the display (prevents us from going to the
- # effort of displaying lines that aren't visible - isn't really a
- # noticeable improvement unless the torrc is bazaarly long)
- if not self.stripComments:
- endingLine = min(len(self.confContents), self.scroll + pageHeight)
- else:
- # checks for the last line of displayable content (ie, non-comment)
- endingLine = self.scroll
- displayedLines = 0 # number of lines of content
- for i in range(self.scroll, len(self.confContents)):
- endingLine += 1
- lineText = self.confContents[i].strip()
-
- if lineText and lineText[0] != "#":
- displayedLines += 1
- if displayedLines == pageHeight: break
-
- for i in range(self.scroll, endingLine):
- lineText = self.confContents[i].strip()
- skipLine = False # true if we're not presenting line due to stripping
-
- command, argument, correction, comment = "", "", "", ""
- commandColor, argumentColor, correctionColor, commentColor = "green", "cyan", "cyan", "white"
-
- if not lineText:
- # no text
- if self.stripComments: skipLine = True
- elif lineText[0] == "#":
- # whole line is commented out
- comment = lineText
- if self.stripComments: skipLine = True
- else:
- # parse out command, argument, and possible comment
- ctlEnd = lineText.find(" ") # end of command
- argEnd = lineText.find("#") # end of argument (start of comment or end of line)
- if argEnd == -1: argEnd = len(lineText)
-
- command, argument, comment = lineText[:ctlEnd], lineText[ctlEnd:argEnd], lineText[argEnd:]
- if self.stripComments: comment = ""
-
- # Tabs print as three spaces. Keeping them as tabs is problematic for
- # the layout since it's counted as a single character, but occupies
- # several cells.
- argument = argument.replace("\t", " ")
-
- # changes presentation if value's incorrect or irrelevant
- if lineNum in self.corrections.keys():
- argumentColor = "red"
- correction = " (%s)" % self.corrections[lineNum]
- elif lineNum in self.irrelevantLines:
- commandColor = "blue"
- argumentColor = "blue"
-
- if not skipLine:
- numOffset = 0 # offset for line numbering
- if self.showLineNum:
- self.addstr(displayLineNum, 0, ("%%%ii" % numFieldWidth) % lineNum, curses.A_BOLD | uiTools.getColor("yellow"))
- numOffset = numFieldWidth + 1
-
- xLoc = 0
- displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, command, curses.A_BOLD | uiTools.getColor(commandColor), numOffset)
- displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, argument, curses.A_BOLD | uiTools.getColor(argumentColor), numOffset)
- displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, correction, curses.A_BOLD | uiTools.getColor(correctionColor), numOffset)
- displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, comment, uiTools.getColor(commentColor), numOffset)
-
- displayLineNum += 1
-
- lineNum += 1
-
Copied: arm/trunk/src/interface/confPanel.py (from rev 22994, arm/trunk/interface/confPanel.py)
===================================================================
--- arm/trunk/src/interface/confPanel.py (rev 0)
+++ arm/trunk/src/interface/confPanel.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,293 @@
+#!/usr/bin/env python
+# confPanel.py -- Presents torrc with syntax highlighting.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import math
+import curses
+import socket
+
+import controller
+from TorCtl import TorCtl
+from util import log, panel, uiTools
+
+# torrc parameters that can be defined multiple times without overwriting
+# from src/or/config.c (entries with LINELIST or LINELIST_S)
+# last updated for tor version 0.2.1.19
+MULTI_LINE_PARAM = ["AlternateBridgeAuthority", "AlternateDirAuthority", "AlternateHSAuthority", "AuthDirBadDir", "AuthDirBadExit", "AuthDirInvalid", "AuthDirReject", "Bridge", "ControlListenAddress", "ControlSocket", "DirListenAddress", "DirPolicy", "DirServer", "DNSListenAddress", "ExitPolicy", "HashedControlPassword", "HiddenServiceDir", "HiddenServiceOptions", "HiddenServicePort", "HiddenServiceVersion", "HiddenServiceAuthorizeClient", "HidServAuth", "Log", "MapAddress", "NatdListenAddress", "NodeFamily", "ORListenAddress", "ReachableAddresses", "ReachableDirAddresses", "ReachableORAddresses", "RecommendedVersions", "RecommendedClientVersions", "RecommendedServerVersions", "SocksListenAddress", "SocksPolicy", "TransListenAddress", "__HashedControlSessionPassword"]
+
+# hidden service options need to be fetched with HiddenServiceOptions
+HIDDEN_SERVICE_PARAM = ["HiddenServiceDir", "HiddenServiceOptions", "HiddenServicePort", "HiddenServiceVersion", "HiddenServiceAuthorizeClient"]
+HIDDEN_SERVICE_FETCH_PARAM = "HiddenServiceOptions"
+
+# size modifiers allowed by config.c
+LABEL_KB = ["kb", "kbyte", "kbytes", "kilobyte", "kilobytes"]
+LABEL_MB = ["m", "mb", "mbyte", "mbytes", "megabyte", "megabytes"]
+LABEL_GB = ["gb", "gbyte", "gbytes", "gigabyte", "gigabytes"]
+LABEL_TB = ["tb", "terabyte", "terabytes"]
+
+# GETCONF aliases (from the _option_abbrevs struct of src/or/config.c)
+# fix for: https://trac.torproject.org/projects/tor/ticket/1798
+# TODO: remove if/when fixed in tor
+# TODO: the following alias entry doesn't work on Tor 0.2.1.19:
+# "HashedControlPassword": "__HashedControlSessionPassword"
+CONF_ALIASES = {"l": "Log",
+ "AllowUnverifiedNodes": "AllowInvalidNodes",
+ "AutomapHostSuffixes": "AutomapHostsSuffixes",
+ "AutomapHostOnResolve": "AutomapHostsOnResolve",
+ "BandwidthRateBytes": "BandwidthRate",
+ "BandwidthBurstBytes": "BandwidthBurst",
+ "DirFetchPostPeriod": "StatusFetchPeriod",
+ "MaxConn": "ConnLimit",
+ "ORBindAddress": "ORListenAddress",
+ "DirBindAddress": "DirListenAddress",
+ "SocksBindAddress": "SocksListenAddress",
+ "UseHelperNodes": "UseEntryGuards",
+ "NumHelperNodes": "NumEntryGuards",
+ "UseEntryNodes": "UseEntryGuards",
+ "NumEntryNodes": "NumEntryGuards",
+ "ResolvConf": "ServerDNSResolvConfFile",
+ "SearchDomains": "ServerDNSSearchDomains",
+ "ServerDNSAllowBrokenResolvConf": "ServerDNSAllowBrokenConfig",
+ "PreferTunnelledDirConns": "PreferTunneledDirConns",
+ "BridgeAuthoritativeDirectory": "BridgeAuthoritativeDir",
+ "StrictEntryNodes": "StrictNodes",
+ "StrictExitNodes": "StrictNodes"}
+
+
+# time modifiers allowed by config.c
+LABEL_MIN = ["minute", "minutes"]
+LABEL_HOUR = ["hour", "hours"]
+LABEL_DAY = ["day", "days"]
+LABEL_WEEK = ["week", "weeks"]
+
+class ConfPanel(panel.Panel):
+ """
+ Presents torrc with syntax highlighting in a scroll-able area.
+ """
+
+ def __init__(self, stdscr, confLocation, conn):
+ panel.Panel.__init__(self, stdscr, "conf", 0)
+ self.confLocation = confLocation
+ self.showLineNum = True
+ self.stripComments = False
+ self.confContents = []
+ self.scroll = 0
+
+ # lines that don't matter due to duplicates
+ self.irrelevantLines = []
+
+ # used to check consistency with tor's actual values - corrections mapping
+ # is of line numbers (one-indexed) to tor's actual values
+ self.corrections = {}
+ self.conn = conn
+
+ self.reset()
+
+ def reset(self, logErrors=True):
+ """
+ Reloads torrc contents and resets scroll height. Returns True if
+ successful, else false.
+ """
+
+ try:
+ resetSuccessful = True
+
+ confFile = open(self.confLocation, "r")
+ self.confContents = confFile.readlines()
+ confFile.close()
+
+ # checks if torrc differs from get_option data
+ self.irrelevantLines = []
+ self.corrections = {}
+ parsedCommands = {} # mapping of parsed commands to line numbers
+
+ for lineNumber in range(len(self.confContents)):
+ lineText = self.confContents[lineNumber].strip()
+
+ if lineText and lineText[0] != "#":
+ # relevant to tor (not blank nor comment)
+ ctlEnd = lineText.find(" ") # end of command
+ argEnd = lineText.find("#") # end of argument (start of comment or end of line)
+ if argEnd == -1: argEnd = len(lineText)
+ command, argument = lineText[:ctlEnd], lineText[ctlEnd:argEnd].strip()
+
+ # replace aliases with the internal representation of the command
+ if command in CONF_ALIASES: command = CONF_ALIASES[command]
+
+ # tor appears to replace tabs with a space, for instance:
+ # "accept\t*:563" is read back as "accept *:563"
+ argument = argument.replace("\t", " ")
+
+ # expands value if it's a size or time
+ comp = argument.strip().lower().split(" ")
+ if len(comp) > 1:
+ size = 0
+ if comp[1] in LABEL_KB: size = int(comp[0]) * 1024
+ elif comp[1] in LABEL_MB: size = int(comp[0]) * 1048576
+ elif comp[1] in LABEL_GB: size = int(comp[0]) * 1073741824
+ elif comp[1] in LABEL_TB: size = int(comp[0]) * 1099511627776
+ elif comp[1] in LABEL_MIN: size = int(comp[0]) * 60
+ elif comp[1] in LABEL_HOUR: size = int(comp[0]) * 3600
+ elif comp[1] in LABEL_DAY: size = int(comp[0]) * 86400
+ elif comp[1] in LABEL_WEEK: size = int(comp[0]) * 604800
+ if size != 0: argument = str(size)
+
+ # most parameters are overwritten if defined multiple times, if so
+ # it's erased from corrections and noted as duplicate instead
+ if not command in MULTI_LINE_PARAM and command in parsedCommands.keys():
+ previousLineNum = parsedCommands[command]
+ self.irrelevantLines.append(previousLineNum)
+ if previousLineNum in self.corrections.keys(): del self.corrections[previousLineNum]
+
+ parsedCommands[command] = lineNumber + 1
+
+ # check validity against tor's actual state
+ try:
+ actualValues = []
+ if command in HIDDEN_SERVICE_PARAM:
+ # hidden services are fetched via a special command
+ hsInfo = self.conn.get_option(HIDDEN_SERVICE_FETCH_PARAM)
+ for entry in hsInfo:
+ if entry[0] == command:
+ actualValues.append(entry[1])
+ break
+ else:
+ # general case - fetch all valid values
+ for key, val in self.conn.get_option(command):
+ if val == None:
+ # TODO: investigate situations where this might occure
+ # (happens if trying to parse HIDDEN_SERVICE_PARAM)
+ if logErrors: log.log(log.WARN, "BUG: Failed to find torrc value for %s" % key)
+ continue
+
+ # TODO: check for a better way of figuring out CSV parameters
+ # (kinda doubt this is right... in config.c its listed as being
+ # a 'LINELIST') - still, good enough for common cases
+ if command in MULTI_LINE_PARAM: toAdd = val.split(",")
+ else: toAdd = [val]
+
+ for newVal in toAdd:
+ newVal = newVal.strip()
+ if newVal not in actualValues: actualValues.append(newVal)
+
+ # there might be multiple values on a single line - if so, check each
+ if command in MULTI_LINE_PARAM and "," in argument:
+ arguments = []
+ for entry in argument.split(","):
+ arguments.append(entry.strip())
+ else:
+ arguments = [argument]
+
+ for entry in arguments:
+ if not entry in actualValues:
+ self.corrections[lineNumber + 1] = ", ".join(actualValues)
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+ if logErrors: log.log(log.WARN, "Unable to validate line %i of the torrc: %s" % (lineNumber + 1, lineText))
+
+ # logs issues that arose
+ if self.irrelevantLines and logErrors:
+ if len(self.irrelevantLines) > 1: first, second, third = "Entries", "are", ", including lines"
+ else: first, second, third = "Entry", "is", " on line"
+ baseMsg = "%s in your torrc %s ignored due to duplication%s" % (first, second, third)
+
+ log.log(log.NOTICE, "%s: %s (highlighted in blue)" % (baseMsg, ", ".join([str(val) for val in self.irrelevantLines])))
+
+ if self.corrections and logErrors:
+ log.log(log.WARN, "Tor's state differs from loaded torrc")
+ except IOError, exc:
+ resetSuccessful = False
+ self.confContents = ["### Unable to load torrc ###"]
+ if logErrors: log.log(log.WARN, "Unable to load torrc (%s)" % str(exc))
+
+ self.scroll = 0
+ return resetSuccessful
+
+ def handleKey(self, key):
+ pageHeight = self.getPreferredSize()[0] - 1
+ 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.confContents) - pageHeight))
+ elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - pageHeight, 0)
+ elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + pageHeight, len(self.confContents) - pageHeight))
+ elif key == ord('n') or key == ord('N'): self.showLineNum = not self.showLineNum
+ elif key == ord('s') or key == ord('S'):
+ self.stripComments = not self.stripComments
+ self.scroll = 0
+ self.redraw(True)
+
+ def draw(self, subwindow, width, height):
+ self.addstr(0, 0, "Tor Config (%s):" % self.confLocation, curses.A_STANDOUT)
+
+ pageHeight = height - 1
+ if self.confContents: numFieldWidth = int(math.log10(len(self.confContents))) + 1
+ else: numFieldWidth = 0 # torrc is blank
+ lineNum, displayLineNum = self.scroll + 1, 1 # lineNum corresponds to torrc, displayLineNum concerns what's presented
+
+ # determine the ending line in the display (prevents us from going to the
+ # effort of displaying lines that aren't visible - isn't really a
+ # noticeable improvement unless the torrc is bazaarly long)
+ if not self.stripComments:
+ endingLine = min(len(self.confContents), self.scroll + pageHeight)
+ else:
+ # checks for the last line of displayable content (ie, non-comment)
+ endingLine = self.scroll
+ displayedLines = 0 # number of lines of content
+ for i in range(self.scroll, len(self.confContents)):
+ endingLine += 1
+ lineText = self.confContents[i].strip()
+
+ if lineText and lineText[0] != "#":
+ displayedLines += 1
+ if displayedLines == pageHeight: break
+
+ for i in range(self.scroll, endingLine):
+ lineText = self.confContents[i].strip()
+ skipLine = False # true if we're not presenting line due to stripping
+
+ command, argument, correction, comment = "", "", "", ""
+ commandColor, argumentColor, correctionColor, commentColor = "green", "cyan", "cyan", "white"
+
+ if not lineText:
+ # no text
+ if self.stripComments: skipLine = True
+ elif lineText[0] == "#":
+ # whole line is commented out
+ comment = lineText
+ if self.stripComments: skipLine = True
+ else:
+ # parse out command, argument, and possible comment
+ ctlEnd = lineText.find(" ") # end of command
+ argEnd = lineText.find("#") # end of argument (start of comment or end of line)
+ if argEnd == -1: argEnd = len(lineText)
+
+ command, argument, comment = lineText[:ctlEnd], lineText[ctlEnd:argEnd], lineText[argEnd:]
+ if self.stripComments: comment = ""
+
+ # Tabs print as three spaces. Keeping them as tabs is problematic for
+ # the layout since it's counted as a single character, but occupies
+ # several cells.
+ argument = argument.replace("\t", " ")
+
+ # changes presentation if value's incorrect or irrelevant
+ if lineNum in self.corrections.keys():
+ argumentColor = "red"
+ correction = " (%s)" % self.corrections[lineNum]
+ elif lineNum in self.irrelevantLines:
+ commandColor = "blue"
+ argumentColor = "blue"
+
+ if not skipLine:
+ numOffset = 0 # offset for line numbering
+ if self.showLineNum:
+ self.addstr(displayLineNum, 0, ("%%%ii" % numFieldWidth) % lineNum, curses.A_BOLD | uiTools.getColor("yellow"))
+ numOffset = numFieldWidth + 1
+
+ xLoc = 0
+ displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, command, curses.A_BOLD | uiTools.getColor(commandColor), numOffset)
+ displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, argument, curses.A_BOLD | uiTools.getColor(argumentColor), numOffset)
+ displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, correction, curses.A_BOLD | uiTools.getColor(correctionColor), numOffset)
+ displayLineNum, xLoc = controller.addstr_wrap(self, displayLineNum, xLoc, comment, uiTools.getColor(commentColor), numOffset)
+
+ displayLineNum += 1
+
+ lineNum += 1
+
Deleted: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py 2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/controller.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,1294 +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 re
-import math
-import time
-import curses
-import socket
-from TorCtl import TorCtl
-from TorCtl import TorUtil
-
-import headerPanel
-import graphing.graphPanel
-import logPanel
-import connPanel
-import confPanel
-import descriptorPopup
-import fileDescriptorPopup
-
-from util import conf, log, connections, hostnames, panel, sysTools, torTools, uiTools
-import graphing.bandwidthStats
-import graphing.connStats
-import graphing.psStats
-
-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"],
- ["torrc"]]
-PAUSEABLE = ["header", "graph", "log", "conn"]
-
-CONFIG = {"logging.rate.refreshRate": 5, "features.graph.type": 1, "features.graph.bw.prepopulate": True, "log.refreshRate": log.DEBUG, "log.configEntryUndefined": 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, subwindow, 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 key not in (curses.KEY_ENTER, 10, ord(' ')):
- 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 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)
-
- # 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)
-
- 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.TOR_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, 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)
- config.update(graphing.graphPanel.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 = ""
-
- # minor refinements for connection resolver
- if not isBlindMode:
- resolver = connections.getResolver("tor")
- if torPid: resolver.processPid = torPid # helps narrow connection results
-
- # 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, config),
- "popup": Popup(stdscr, 9),
- "graph": graphing.graphPanel.GraphPanel(stdscr),
- "log": logPanel.LogMonitor(stdscr, conn, loggedEvents)}
-
- # 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"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
- panels["control"] = ControlPanel(stdscr, isBlindMode)
- panels["torrc"] = confPanel.ConfPanel(stdscr, confLocation, conn)
-
- # 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.psStats.PsStats(config))
- 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(panels["conn"])
- 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
-
- # directs logged TorCtl events to log panel
- TorUtil.loglevel = "DEBUG"
- TorUtil.logfile = panels["log"]
-
- # tells revised panels to run as daemons
- panels["header"].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"], "unrecognized 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)
- 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
- panels["conn"].resetOptions()
- #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"].getPreferredHeight())
-
- panels["torrc"].reset()
- 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")
-
- panels["conn"].reset()
-
- # 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":
- if panelKey in ("header", "graph"):
- # revised panel (handles its own content refreshing)
- panels[panelKey].redraw()
- else:
- panels[panelKey].redraw(True)
-
- stdscr.refresh()
-
- currentTime = time.time()
- if currentTime - lastPerformanceLog >= CONFIG["logging.rate.refreshRate"]:
- log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds" % (currentTime - redrawStartTime))
- lastPerformanceLog = 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?)
-
- # joins on utility daemon threads - this might take a moment since
- # the internal threadpools being joined might be sleeping
- resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
- if resolver: resolver.stop() # sets halt flag (returning immediately)
- hostnames.stop() # halts and joins on hostname worker thread pool
- if resolver: resolver.join() # joins on halted resolver
-
- # stops panel daemons
- panels["header"].stop()
- panels["header"].join()
-
- 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)
-
- 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('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>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
- popup.addfstr(1, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
- popup.addfstr(2, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % graphing.graphPanel.BOUND_LABELS[panels["graph"].bounds])
- popup.addfstr(2, 41, "<b>d</b>: file descriptors")
- popup.addfstr(3, 2, "<b>e</b>: change logged events")
-
- regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
- popup.addfstr(3, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
-
- pageOverrideKeys = (ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'))
- 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>: connection details")
- popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
-
- listingType = connPanel.LIST_LABEL[panels["conn"].listingType].lower()
- popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
-
- resolverUtil = connections.getResolver("tor").overwriteResolver
- if resolverUtil == None: resolverUtil = "auto"
- else: resolverUtil = connections.CMD_STR[resolverUtil]
- popup.addfstr(4, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
-
- allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
- popup.addfstr(5, 2, "<b>r</b>: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
-
- popup.addfstr(5, 41, "<b>s</b>: sort ordering")
- popup.addfstr(6, 2, "<b>c</b>: client circuits")
-
- #popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
-
- pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('c'))
- 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)")
-
- 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)
- 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 = (panels["graph"].bounds + 1) % 3
-
- selectiveRefresh(panels, page)
- elif page == 0 and key in (ord('d'), ord('D')):
- # provides popup with file descriptors
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
-
- fileDescriptorPopup.showFileDescriptorPopup(panels["popup"], stdscr, torPid)
-
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
- 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)
-
- # makes cursor and typing visible
- try: curses.curs_set(1)
- except curses.error: pass
- curses.echo()
-
- # 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"].win.getstr(0, 15)
- eventsInput = eventsInput.replace(' ', '') # strips spaces
-
- # reverts visability settings
- try: curses.curs_set(0)
- except curses.error: pass
- curses.noecho()
- curses.halfdelay(REFRESH_RATE * 10) # evidenlty previous tweaks reset this...
-
- # 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"].loggedEvents = 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()
- 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"].regexFilter = 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)
-
- # makes cursor and typing visible
- try: curses.curs_set(1)
- except curses.error: pass
- curses.echo()
-
- # gets user input (this blocks monitor updates)
- regexInput = panels["control"].win.getstr(0, 20)
-
- # reverts visability settings
- try: curses.curs_set(0)
- except curses.error: pass
- curses.noecho()
- curses.halfdelay(REFRESH_RATE * 10)
-
- if regexInput != "":
- try:
- panels["log"].regexFilter = 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"].regexFilter = 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)
- elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
- # canceling hostname resolution (esc on any page)
- panels["conn"].listingType = connPanel.LIST_IP
- panels["control"].resolvingCounter = -1
- hostnames.setPaused(True)
- panels["conn"].sortConnections()
- elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
- # provides details on selected connection
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
- popup = panels["popup"]
-
- # reconfigures connection panel to accomidate details dialog
- panels["conn"].showLabel = False
- panels["conn"].showingDetails = True
- panels["conn"].redraw(True)
-
- hostnames.setPaused(not panels["conn"].allowDNS)
- relayLookupCache = {} # temporary cache of entry -> (ns data, desc data)
-
- curses.cbreak() # wait indefinitely for key presses (no timeout)
- key = 0
-
- while key not in (curses.KEY_ENTER, 10, ord(' ')):
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Connection Details:", curses.A_STANDOUT)
-
- selection = panels["conn"].cursorSelection
- if not selection or not panels["conn"].connections: break
- selectionColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
- format = uiTools.getColor(selectionColor) | curses.A_BOLD
-
- selectedIp = selection[connPanel.CONN_F_IP]
- selectedPort = selection[connPanel.CONN_F_PORT]
- selectedIsPrivate = selection[connPanel.CONN_PRIVATE]
-
- addrLabel = "address: %s:%s" % (selectedIp, selectedPort)
-
- if selection[connPanel.CONN_TYPE] == "family" and int(selection[connPanel.CONN_L_PORT]) > 65535:
- # unresolved family entry - unknown ip/port
- addrLabel = "address: unknown"
-
- if selectedIsPrivate: hostname = None
- else:
- try: hostname = hostnames.resolve(selectedIp)
- except ValueError: hostname = "unknown" # hostname couldn't be resolved
-
- if hostname == None:
- if hostnames.isPaused() or selectedIsPrivate: hostname = "DNS resolution disallowed"
- else:
- # if hostname is still being resolved refresh panel every half-second until it's completed
- curses.halfdelay(5)
- hostname = "resolving..."
- elif len(hostname) > 73 - len(addrLabel):
- # hostname too long - truncate
- hostname = "%s..." % hostname[:70 - len(addrLabel)]
-
- if selectedIsPrivate:
- popup.addstr(1, 2, "address: <scrubbed> (unknown)", format)
- popup.addstr(2, 2, "locale: ??", format)
- popup.addstr(3, 2, "No consensus data found", format)
- else:
- popup.addstr(1, 2, "%s (%s)" % (addrLabel, hostname), format)
-
- locale = selection[connPanel.CONN_COUNTRY]
- popup.addstr(2, 2, "locale: %s" % locale, format)
-
- # provides consensus data for selection (needs fingerprint to get anywhere...)
- fingerprint = panels["conn"].getFingerprint(selectedIp, selectedPort)
-
- if fingerprint == "UNKNOWN":
- if selectedIp not in panels["conn"].fingerprintMappings.keys():
- # no consensus entry for this ip address
- popup.addstr(3, 2, "No consensus data found", format)
- else:
- # couldn't resolve due to multiple matches - list them all
- popup.addstr(3, 2, "Muliple matches, possible fingerprints are:", format)
- matchings = panels["conn"].fingerprintMappings[selectedIp]
-
- line = 4
- for (matchPort, matchFingerprint, matchNickname) in matchings:
- popup.addstr(line, 2, "%i. or port: %-5s fingerprint: %s" % (line - 3, matchPort, matchFingerprint), format)
- line += 1
-
- if line == 7 and len(matchings) > 4:
- popup.addstr(8, 2, "... %i more" % len(matchings) - 3, format)
- break
- else:
- # fingerprint found - retrieve related data
- lookupErrored = False
- if selection in relayLookupCache.keys(): nsEntry, descEntry = relayLookupCache[selection]
- else:
- try:
- nsCall = conn.get_network_status("id/%s" % fingerprint)
- if len(nsCall) == 0: raise TorCtl.ErrorReply() # no results provided
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
- # ns lookup fails or provides empty results - can happen with
- # localhost lookups if relay's having problems (orport not
- # reachable) and this will be empty if network consensus
- # couldn't be fetched
- lookupErrored = True
-
- if not lookupErrored and nsCall:
- if len(nsCall) > 1:
- # multiple records for fingerprint (shouldn't happen)
- log.log(log.WARN, "Multiple consensus entries for fingerprint: %s" % fingerprint)
-
- nsEntry = nsCall[0]
-
- try:
- descLookupCmd = "desc/id/%s" % fingerprint
- descEntry = TorCtl.Router.build_from_desc(conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
- relayLookupCache[selection] = (nsEntry, descEntry)
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): lookupErrored = True # desc lookup failed
-
- if lookupErrored:
- popup.addstr(3, 2, "Unable to retrieve consensus data", format)
- else:
- popup.addstr(2, 15, "fingerprint: %s" % fingerprint, format)
-
- nickname = panels["conn"].getNickname(selectedIp, selectedPort)
- dirPortLabel = "dirport: %i" % nsEntry.dirport if nsEntry.dirport else ""
- popup.addstr(3, 2, "nickname: %-25s orport: %-10i %s" % (nickname, nsEntry.orport, dirPortLabel), format)
-
- popup.addstr(4, 2, "published: %-24s os: %-14s version: %s" % (descEntry.published, descEntry.os, descEntry.version), format)
- popup.addstr(5, 2, "flags: %s" % ", ".join(nsEntry.flags), format)
-
- exitLine = ", ".join([str(k) for k in descEntry.exitpolicy])
- if len(exitLine) > 63: exitLine = "%s..." % exitLine[:60]
- popup.addstr(6, 2, "exit policy: %s" % exitLine, format)
-
- if descEntry.contact:
- # clears up some common obscuring
- contactAddr = descEntry.contact
- obscuring = [(" at ", "@"), (" AT ", "@"), ("AT", "@"), (" dot ", "."), (" DOT ", ".")]
- for match, replace in obscuring: contactAddr = contactAddr.replace(match, replace)
- if len(contactAddr) > 67: contactAddr = "%s..." % contactAddr[:64]
- popup.addstr(7, 2, "contact: %s" % contactAddr, format)
-
- popup.refresh()
- key = stdscr.getch()
-
- if key == curses.KEY_RIGHT: key = curses.KEY_DOWN
- elif key == curses.KEY_LEFT: key = curses.KEY_UP
-
- if key in (curses.KEY_DOWN, curses.KEY_UP, curses.KEY_PPAGE, curses.KEY_NPAGE):
- panels["conn"].handleKey(key)
- elif key in (ord('d'), ord('D')):
- descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
- panels["conn"].redraw(True)
-
- panels["conn"].showLabel = True
- panels["conn"].showingDetails = False
- hostnames.setPaused(not panels["conn"].allowDNS and panels["conn"].listingType == connPanel.LIST_HOSTNAME)
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
- elif page == 1 and panels["conn"].isCursorEnabled 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)
- panels["conn"].showLabel = False
- panels["conn"].redraw(True)
-
- descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
-
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- panels["conn"].showLabel = True
- finally:
- panel.CURSES_LOCK.release()
- elif page == 1 and (key == ord('l') or key == ord('L')):
- # provides menu to pick identification info listed for connections
- optionTypes = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
- options = [connPanel.LIST_LABEL[sortType] for sortType in optionTypes]
- initialSelection = panels["conn"].listingType # enums correspond to index
-
- # hides top label of conn panel and pauses panels
- panels["conn"].showLabel = False
- 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"].showLabel = True
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and optionTypes[selection] != panels["conn"].listingType:
- panels["conn"].listingType = optionTypes[selection]
-
- if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
- curses.halfdelay(10) # refreshes display every second until done resolving
- panels["control"].resolvingCounter = hostnames.getRequestCount() - hostnames.getPendingCount()
-
- hostnames.setPaused(not panels["conn"].allowDNS)
- for connEntry in panels["conn"].connections:
- try: hostnames.resolve(connEntry[connPanel.CONN_F_IP])
- except ValueError: pass
- else:
- panels["control"].resolvingCounter = -1
- hostnames.setPaused(True)
-
- panels["conn"].sortConnections()
- elif page == 1 and (key == ord('u') or key == ord('U')):
- # provides menu to pick identification resolving utility
- optionTypes = [None, connections.CMD_NETSTAT, connections.CMD_SS, connections.CMD_LSOF]
- options = ["auto"] + [connections.CMD_STR[util] for util in optionTypes[1:]]
-
- initialSelection = connections.getResolver("tor").overwriteResolver # enums correspond to indices
- if initialSelection == None: initialSelection = 0
-
- # hides top label of conn panel and pauses panels
- panels["conn"].showLabel = False
- panels["conn"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
-
- # reverts changes made for popup
- panels["conn"].showLabel = True
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and optionTypes[selection] != connections.getResolver("tor").overwriteResolver:
- connections.getResolver("tor").overwriteResolver = optionTypes[selection]
- elif page == 1 and (key == ord('s') or key == ord('S')):
- # set ordering for connection listing
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
-
- # lists event types
- popup = panels["popup"]
- selections = [] # new ordering
- cursorLoc = 0 # index of highlighted option
-
- # listing of inital ordering
- prevOrdering = "<b>Current Order: "
- for sort in panels["conn"].sortOrdering: prevOrdering += connPanel.getSortLabel(sort, True) + ", "
- prevOrdering = prevOrdering[:-2] + "</b>"
-
- # Makes listing of all options
- options = []
- for (type, label, func) in connPanel.SORT_TYPES: options.append(connPanel.getSortLabel(type))
- options.append("Cancel")
-
- while len(selections) < 3:
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Connection Ordering:", curses.A_STANDOUT)
- popup.addfstr(1, 2, prevOrdering)
-
- # provides new ordering
- newOrdering = "<b>New Order: "
- if selections:
- for sort in selections: newOrdering += connPanel.getSortLabel(sort, True) + ", "
- newOrdering = newOrdering[:-2] + "</b>"
- else: newOrdering += "</b>"
- popup.addfstr(2, 2, newOrdering)
-
- row, col, index = 4, 0, 0
- for option in options:
- popup.addstr(row, col * 19 + 2, option, curses.A_STANDOUT if cursorLoc == index else curses.A_NORMAL)
- col += 1
- index += 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(options) - 1, cursorLoc + 1)
- elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4)
- elif key == curses.KEY_DOWN: cursorLoc = min(len(options) - 1, cursorLoc + 4)
- elif key in (curses.KEY_ENTER, 10, ord(' ')):
- # selected entry (the ord of '10' seems needed to pick up enter)
- selection = options[cursorLoc]
- if selection == "Cancel": break
- else:
- selections.append(connPanel.getSortType(selection.replace("Tor ID", "Fingerprint")))
- options.remove(selection)
- cursorLoc = min(cursorLoc, len(options) - 1)
- elif key == 27: break # esc - cancel
-
- if len(selections) == 3:
- panels["conn"].sortOrdering = selections
- panels["conn"].sortConnections()
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
- elif page == 1 and (key == ord('c') or key == ord('C')):
- # displays popup with client circuits
- clientCircuits = None
- try:
- clientCircuits = conn.get_info("circuit-status")["circuit-status"].split("\n")
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
-
- maxEntryLength = 0
- if clientCircuits:
- for clientEntry in clientCircuits: maxEntryLength = max(len(clientEntry), maxEntryLength)
-
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # makes sure there's room for the longest entry
- popup = panels["popup"]
- if clientCircuits and maxEntryLength + 4 > popup.getPreferredSize()[1]:
- popup.height = max(popup.height, len(clientCircuits) + 3)
- popup.recreate(stdscr, maxEntryLength + 4)
-
- # lists commands
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Client Circuits:", curses.A_STANDOUT)
-
- if clientCircuits == None:
- popup.addstr(1, 2, "Unable to retireve current circuits")
- elif len(clientCircuits) == 1 and clientCircuits[0] == "":
- popup.addstr(1, 2, "No active client circuits")
- else:
- line = 1
- for clientEntry in clientCircuits:
- popup.addstr(line, 2, clientEntry)
- line += 1
-
- popup.addstr(popup.height - 2, 2, "Press any key...")
- popup.refresh()
-
- curses.cbreak()
- stdscr.getch()
- curses.halfdelay(REFRESH_RATE * 10)
-
- # reverts popup dimensions
- popup.height = 9
- popup.recreate(stdscr, 80)
-
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
- elif page == 2 and key == ord('r') or key == ord('R'):
- # reloads torrc, providing a notice if successful or not
- isSuccessful = panels["torrc"].reset(False)
- resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
- if isSuccessful: 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 == 2 and (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" % str(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 page == 0:
- panels["log"].handleKey(key)
- elif page == 1:
- panels["conn"].handleKey(key)
- elif page == 2:
- panels["torrc"].handleKey(key)
-
-def startTorMonitor(loggedEvents, isBlindMode):
- try:
- curses.wrapper(drawTorMonitor, loggedEvents, isBlindMode)
- except KeyboardInterrupt:
- pass # skip printing stack trace in case of keyboard interrupt
-
Copied: arm/trunk/src/interface/controller.py (from rev 22991, arm/trunk/interface/controller.py)
===================================================================
--- arm/trunk/src/interface/controller.py (rev 0)
+++ arm/trunk/src/interface/controller.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,1313 @@
+#!/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 re
+import math
+import time
+import curses
+import socket
+from TorCtl import TorCtl
+from TorCtl import TorUtil
+
+import headerPanel
+import graphing.graphPanel
+import logPanel
+import connPanel
+import confPanel
+import descriptorPopup
+import fileDescriptorPopup
+
+from util import conf, log, connections, hostnames, panel, sysTools, torTools, uiTools
+import graphing.bandwidthStats
+import graphing.connStats
+import graphing.psStats
+
+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"],
+ ["torrc"]]
+PAUSEABLE = ["header", "graph", "log", "conn"]
+
+CONFIG = {"logging.rate.refreshRate": 5, "features.graph.type": 1, "features.graph.bw.prepopulate": True, "log.refreshRate": log.DEBUG, "log.configEntryUndefined": 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, subwindow, 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 key not in (curses.KEY_ENTER, 10, ord(' ')):
+ 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 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)
+
+ # 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)
+
+ 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.TOR_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, 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)
+
+ # 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 = ""
+
+ # minor refinements for connection resolver
+ if not isBlindMode:
+ resolver = connections.getResolver("tor")
+ if torPid: resolver.processPid = torPid # helps narrow connection results
+
+ # 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, config),
+ "popup": Popup(stdscr, 9),
+ "graph": graphing.graphPanel.GraphPanel(stdscr),
+ "log": logPanel.LogMonitor(stdscr, conn, loggedEvents)}
+
+ # 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"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
+ panels["control"] = ControlPanel(stdscr, isBlindMode)
+ panels["torrc"] = confPanel.ConfPanel(stdscr, confLocation, conn)
+
+ # 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.psStats.PsStats(config))
+ 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(panels["conn"])
+ 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
+
+ # directs logged TorCtl events to log panel
+ TorUtil.loglevel = "DEBUG"
+ TorUtil.logfile = panels["log"]
+
+ # tells revised panels to run as daemons
+ panels["header"].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"], "unrecognized 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)
+ 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
+ panels["conn"].resetOptions()
+ #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"].getPreferredHeight())
+
+ panels["torrc"].reset()
+ 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")
+
+ panels["conn"].reset()
+
+ # 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":
+ if panelKey in ("header", "graph"):
+ # revised panel (handles its own content refreshing)
+ panels[panelKey].redraw()
+ else:
+ panels[panelKey].redraw(True)
+
+ stdscr.refresh()
+
+ currentTime = time.time()
+ if currentTime - lastPerformanceLog >= CONFIG["logging.rate.refreshRate"]:
+ log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds" % (currentTime - redrawStartTime))
+ lastPerformanceLog = 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?)
+
+ # joins on utility daemon threads - this might take a moment since
+ # the internal threadpools being joined might be sleeping
+ resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
+ if resolver: resolver.stop() # sets halt flag (returning immediately)
+ hostnames.stop() # halts and joins on hostname worker thread pool
+ if resolver: resolver.join() # joins on halted resolver
+
+ # stops panel daemons
+ panels["header"].stop()
+ panels["header"].join()
+
+ 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)
+
+ 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('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>)" % graphing.graphPanel.BOUND_LABELS[panels["graph"].bounds])
+ popup.addfstr(4, 41, "<b>d</b>: file descriptors")
+ 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)
+
+ pageOverrideKeys = (ord('m'), ord('n'), ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'))
+ 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>: connection details")
+ popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
+
+ listingType = connPanel.LIST_LABEL[panels["conn"].listingType].lower()
+ popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
+
+ resolverUtil = connections.getResolver("tor").overwriteResolver
+ if resolverUtil == None: resolverUtil = "auto"
+ else: resolverUtil = connections.CMD_STR[resolverUtil]
+ popup.addfstr(4, 41, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
+
+ allowDnsLabel = "allow" if panels["conn"].allowDNS else "disallow"
+ popup.addfstr(5, 2, "<b>r</b>: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
+
+ popup.addfstr(5, 41, "<b>s</b>: sort ordering")
+ popup.addfstr(6, 2, "<b>c</b>: client circuits")
+
+ #popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
+
+ pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('c'))
+ 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)")
+
+ 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)
+ 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 = (panels["graph"].bounds + 1) % 3
+
+ selectiveRefresh(panels, page)
+ elif page == 0 and key in (ord('d'), ord('D')):
+ # provides popup with file descriptors
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+
+ fileDescriptorPopup.showFileDescriptorPopup(panels["popup"], stdscr, torPid)
+
+ setPauseState(panels, isPaused, page)
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ panel.CURSES_LOCK.release()
+ 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)
+
+ # makes cursor and typing visible
+ try: curses.curs_set(1)
+ except curses.error: pass
+ curses.echo()
+
+ # 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"].win.getstr(0, 15)
+ eventsInput = eventsInput.replace(' ', '') # strips spaces
+
+ # reverts visability settings
+ try: curses.curs_set(0)
+ except curses.error: pass
+ curses.noecho()
+ curses.halfdelay(REFRESH_RATE * 10) # evidenlty previous tweaks reset this...
+
+ # 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"].loggedEvents = 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()
+ 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"].regexFilter = 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)
+
+ # makes cursor and typing visible
+ try: curses.curs_set(1)
+ except curses.error: pass
+ curses.echo()
+
+ # gets user input (this blocks monitor updates)
+ regexInput = panels["control"].win.getstr(0, 20)
+
+ # reverts visability settings
+ try: curses.curs_set(0)
+ except curses.error: pass
+ curses.noecho()
+ curses.halfdelay(REFRESH_RATE * 10)
+
+ if regexInput != "":
+ try:
+ panels["log"].regexFilter = 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"].regexFilter = 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)
+ 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 key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
+ # canceling hostname resolution (esc on any page)
+ panels["conn"].listingType = connPanel.LIST_IP
+ panels["control"].resolvingCounter = -1
+ hostnames.setPaused(True)
+ panels["conn"].sortConnections()
+ elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
+ # provides details on selected connection
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+ popup = panels["popup"]
+
+ # reconfigures connection panel to accomidate details dialog
+ panels["conn"].showLabel = False
+ panels["conn"].showingDetails = True
+ panels["conn"].redraw(True)
+
+ hostnames.setPaused(not panels["conn"].allowDNS)
+ relayLookupCache = {} # temporary cache of entry -> (ns data, desc data)
+
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+ key = 0
+
+ while key not in (curses.KEY_ENTER, 10, ord(' ')):
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Connection Details:", curses.A_STANDOUT)
+
+ selection = panels["conn"].cursorSelection
+ if not selection or not panels["conn"].connections: break
+ selectionColor = connPanel.TYPE_COLORS[selection[connPanel.CONN_TYPE]]
+ format = uiTools.getColor(selectionColor) | curses.A_BOLD
+
+ selectedIp = selection[connPanel.CONN_F_IP]
+ selectedPort = selection[connPanel.CONN_F_PORT]
+ selectedIsPrivate = selection[connPanel.CONN_PRIVATE]
+
+ addrLabel = "address: %s:%s" % (selectedIp, selectedPort)
+
+ if selection[connPanel.CONN_TYPE] == "family" and int(selection[connPanel.CONN_L_PORT]) > 65535:
+ # unresolved family entry - unknown ip/port
+ addrLabel = "address: unknown"
+
+ if selectedIsPrivate: hostname = None
+ else:
+ try: hostname = hostnames.resolve(selectedIp)
+ except ValueError: hostname = "unknown" # hostname couldn't be resolved
+
+ if hostname == None:
+ if hostnames.isPaused() or selectedIsPrivate: hostname = "DNS resolution disallowed"
+ else:
+ # if hostname is still being resolved refresh panel every half-second until it's completed
+ curses.halfdelay(5)
+ hostname = "resolving..."
+ elif len(hostname) > 73 - len(addrLabel):
+ # hostname too long - truncate
+ hostname = "%s..." % hostname[:70 - len(addrLabel)]
+
+ if selectedIsPrivate:
+ popup.addstr(1, 2, "address: <scrubbed> (unknown)", format)
+ popup.addstr(2, 2, "locale: ??", format)
+ popup.addstr(3, 2, "No consensus data found", format)
+ else:
+ popup.addstr(1, 2, "%s (%s)" % (addrLabel, hostname), format)
+
+ locale = selection[connPanel.CONN_COUNTRY]
+ popup.addstr(2, 2, "locale: %s" % locale, format)
+
+ # provides consensus data for selection (needs fingerprint to get anywhere...)
+ fingerprint = panels["conn"].getFingerprint(selectedIp, selectedPort)
+
+ if fingerprint == "UNKNOWN":
+ if selectedIp not in panels["conn"].fingerprintMappings.keys():
+ # no consensus entry for this ip address
+ popup.addstr(3, 2, "No consensus data found", format)
+ else:
+ # couldn't resolve due to multiple matches - list them all
+ popup.addstr(3, 2, "Muliple matches, possible fingerprints are:", format)
+ matchings = panels["conn"].fingerprintMappings[selectedIp]
+
+ line = 4
+ for (matchPort, matchFingerprint, matchNickname) in matchings:
+ popup.addstr(line, 2, "%i. or port: %-5s fingerprint: %s" % (line - 3, matchPort, matchFingerprint), format)
+ line += 1
+
+ if line == 7 and len(matchings) > 4:
+ popup.addstr(8, 2, "... %i more" % len(matchings) - 3, format)
+ break
+ else:
+ # fingerprint found - retrieve related data
+ lookupErrored = False
+ if selection in relayLookupCache.keys(): nsEntry, descEntry = relayLookupCache[selection]
+ else:
+ try:
+ nsCall = conn.get_network_status("id/%s" % fingerprint)
+ if len(nsCall) == 0: raise TorCtl.ErrorReply() # no results provided
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+ # ns lookup fails or provides empty results - can happen with
+ # localhost lookups if relay's having problems (orport not
+ # reachable) and this will be empty if network consensus
+ # couldn't be fetched
+ lookupErrored = True
+
+ if not lookupErrored and nsCall:
+ if len(nsCall) > 1:
+ # multiple records for fingerprint (shouldn't happen)
+ log.log(log.WARN, "Multiple consensus entries for fingerprint: %s" % fingerprint)
+
+ nsEntry = nsCall[0]
+
+ try:
+ descLookupCmd = "desc/id/%s" % fingerprint
+ descEntry = TorCtl.Router.build_from_desc(conn.get_info(descLookupCmd)[descLookupCmd].split("\n"), nsEntry)
+ relayLookupCache[selection] = (nsEntry, descEntry)
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): lookupErrored = True # desc lookup failed
+
+ if lookupErrored:
+ popup.addstr(3, 2, "Unable to retrieve consensus data", format)
+ else:
+ popup.addstr(2, 15, "fingerprint: %s" % fingerprint, format)
+
+ nickname = panels["conn"].getNickname(selectedIp, selectedPort)
+ dirPortLabel = "dirport: %i" % nsEntry.dirport if nsEntry.dirport else ""
+ popup.addstr(3, 2, "nickname: %-25s orport: %-10i %s" % (nickname, nsEntry.orport, dirPortLabel), format)
+
+ popup.addstr(4, 2, "published: %-24s os: %-14s version: %s" % (descEntry.published, descEntry.os, descEntry.version), format)
+ popup.addstr(5, 2, "flags: %s" % ", ".join(nsEntry.flags), format)
+
+ exitLine = ", ".join([str(k) for k in descEntry.exitpolicy])
+ if len(exitLine) > 63: exitLine = "%s..." % exitLine[:60]
+ popup.addstr(6, 2, "exit policy: %s" % exitLine, format)
+
+ if descEntry.contact:
+ # clears up some common obscuring
+ contactAddr = descEntry.contact
+ obscuring = [(" at ", "@"), (" AT ", "@"), ("AT", "@"), (" dot ", "."), (" DOT ", ".")]
+ for match, replace in obscuring: contactAddr = contactAddr.replace(match, replace)
+ if len(contactAddr) > 67: contactAddr = "%s..." % contactAddr[:64]
+ popup.addstr(7, 2, "contact: %s" % contactAddr, format)
+
+ popup.refresh()
+ key = stdscr.getch()
+
+ if key == curses.KEY_RIGHT: key = curses.KEY_DOWN
+ elif key == curses.KEY_LEFT: key = curses.KEY_UP
+
+ if key in (curses.KEY_DOWN, curses.KEY_UP, curses.KEY_PPAGE, curses.KEY_NPAGE):
+ panels["conn"].handleKey(key)
+ elif key in (ord('d'), ord('D')):
+ descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
+ panels["conn"].redraw(True)
+
+ panels["conn"].showLabel = True
+ panels["conn"].showingDetails = False
+ hostnames.setPaused(not panels["conn"].allowDNS and panels["conn"].listingType == connPanel.LIST_HOSTNAME)
+ setPauseState(panels, isPaused, page)
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 1 and panels["conn"].isCursorEnabled 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)
+ panels["conn"].showLabel = False
+ panels["conn"].redraw(True)
+
+ descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, conn, panels["conn"])
+
+ setPauseState(panels, isPaused, page)
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ panels["conn"].showLabel = True
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 1 and (key == ord('l') or key == ord('L')):
+ # provides menu to pick identification info listed for connections
+ optionTypes = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
+ options = [connPanel.LIST_LABEL[sortType] for sortType in optionTypes]
+ initialSelection = panels["conn"].listingType # enums correspond to index
+
+ # hides top label of conn panel and pauses panels
+ panels["conn"].showLabel = False
+ 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"].showLabel = True
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1 and optionTypes[selection] != panels["conn"].listingType:
+ panels["conn"].listingType = optionTypes[selection]
+
+ if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
+ curses.halfdelay(10) # refreshes display every second until done resolving
+ panels["control"].resolvingCounter = hostnames.getRequestCount() - hostnames.getPendingCount()
+
+ hostnames.setPaused(not panels["conn"].allowDNS)
+ for connEntry in panels["conn"].connections:
+ try: hostnames.resolve(connEntry[connPanel.CONN_F_IP])
+ except ValueError: pass
+ else:
+ panels["control"].resolvingCounter = -1
+ hostnames.setPaused(True)
+
+ panels["conn"].sortConnections()
+ elif page == 1 and (key == ord('u') or key == ord('U')):
+ # provides menu to pick identification resolving utility
+ optionTypes = [None, connections.CMD_NETSTAT, connections.CMD_SS, connections.CMD_LSOF]
+ options = ["auto"] + [connections.CMD_STR[util] for util in optionTypes[1:]]
+
+ initialSelection = connections.getResolver("tor").overwriteResolver # enums correspond to indices
+ if initialSelection == None: initialSelection = 0
+
+ # hides top label of conn panel and pauses panels
+ panels["conn"].showLabel = False
+ panels["conn"].redraw(True)
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
+
+ # reverts changes made for popup
+ panels["conn"].showLabel = True
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1 and optionTypes[selection] != connections.getResolver("tor").overwriteResolver:
+ connections.getResolver("tor").overwriteResolver = optionTypes[selection]
+ elif page == 1 and (key == ord('s') or key == ord('S')):
+ # set ordering for connection listing
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+
+ # lists event types
+ popup = panels["popup"]
+ selections = [] # new ordering
+ cursorLoc = 0 # index of highlighted option
+
+ # listing of inital ordering
+ prevOrdering = "<b>Current Order: "
+ for sort in panels["conn"].sortOrdering: prevOrdering += connPanel.getSortLabel(sort, True) + ", "
+ prevOrdering = prevOrdering[:-2] + "</b>"
+
+ # Makes listing of all options
+ options = []
+ for (type, label, func) in connPanel.SORT_TYPES: options.append(connPanel.getSortLabel(type))
+ options.append("Cancel")
+
+ while len(selections) < 3:
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Connection Ordering:", curses.A_STANDOUT)
+ popup.addfstr(1, 2, prevOrdering)
+
+ # provides new ordering
+ newOrdering = "<b>New Order: "
+ if selections:
+ for sort in selections: newOrdering += connPanel.getSortLabel(sort, True) + ", "
+ newOrdering = newOrdering[:-2] + "</b>"
+ else: newOrdering += "</b>"
+ popup.addfstr(2, 2, newOrdering)
+
+ row, col, index = 4, 0, 0
+ for option in options:
+ popup.addstr(row, col * 19 + 2, option, curses.A_STANDOUT if cursorLoc == index else curses.A_NORMAL)
+ col += 1
+ index += 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(options) - 1, cursorLoc + 1)
+ elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4)
+ elif key == curses.KEY_DOWN: cursorLoc = min(len(options) - 1, cursorLoc + 4)
+ elif key in (curses.KEY_ENTER, 10, ord(' ')):
+ # selected entry (the ord of '10' seems needed to pick up enter)
+ selection = options[cursorLoc]
+ if selection == "Cancel": break
+ else:
+ selections.append(connPanel.getSortType(selection.replace("Tor ID", "Fingerprint")))
+ options.remove(selection)
+ cursorLoc = min(cursorLoc, len(options) - 1)
+ elif key == 27: break # esc - cancel
+
+ if len(selections) == 3:
+ panels["conn"].sortOrdering = selections
+ panels["conn"].sortConnections()
+ setPauseState(panels, isPaused, page)
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 1 and (key == ord('c') or key == ord('C')):
+ # displays popup with client circuits
+ clientCircuits = None
+ try:
+ clientCircuits = conn.get_info("circuit-status")["circuit-status"].split("\n")
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+
+ maxEntryLength = 0
+ if clientCircuits:
+ for clientEntry in clientCircuits: maxEntryLength = max(len(clientEntry), maxEntryLength)
+
+ panel.CURSES_LOCK.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # makes sure there's room for the longest entry
+ popup = panels["popup"]
+ if clientCircuits and maxEntryLength + 4 > popup.getPreferredSize()[1]:
+ popup.height = max(popup.height, len(clientCircuits) + 3)
+ popup.recreate(stdscr, maxEntryLength + 4)
+
+ # lists commands
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Client Circuits:", curses.A_STANDOUT)
+
+ if clientCircuits == None:
+ popup.addstr(1, 2, "Unable to retireve current circuits")
+ elif len(clientCircuits) == 1 and clientCircuits[0] == "":
+ popup.addstr(1, 2, "No active client circuits")
+ else:
+ line = 1
+ for clientEntry in clientCircuits:
+ popup.addstr(line, 2, clientEntry)
+ line += 1
+
+ popup.addstr(popup.height - 2, 2, "Press any key...")
+ popup.refresh()
+
+ curses.cbreak()
+ stdscr.getch()
+ curses.halfdelay(REFRESH_RATE * 10)
+
+ # reverts popup dimensions
+ popup.height = 9
+ popup.recreate(stdscr, 80)
+
+ setPauseState(panels, isPaused, page)
+ finally:
+ panel.CURSES_LOCK.release()
+ elif page == 2 and key == ord('r') or key == ord('R'):
+ # reloads torrc, providing a notice if successful or not
+ isSuccessful = panels["torrc"].reset(False)
+ resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
+ if isSuccessful: 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 == 2 and (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" % str(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 page == 0:
+ panels["log"].handleKey(key)
+ elif page == 1:
+ panels["conn"].handleKey(key)
+ elif page == 2:
+ panels["torrc"].handleKey(key)
+
+def startTorMonitor(loggedEvents, isBlindMode):
+ try:
+ curses.wrapper(drawTorMonitor, loggedEvents, isBlindMode)
+ except KeyboardInterrupt:
+ pass # skip printing stack trace in case of keyboard interrupt
+
Deleted: arm/trunk/src/interface/graphing/bandwidthStats.py
===================================================================
--- arm/trunk/interface/graphing/bandwidthStats.py 2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/graphing/bandwidthStats.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,354 +0,0 @@
-"""
-Tracks bandwidth usage of the tor process, expanding to include accounting
-stats if they're set.
-"""
-
-import time
-
-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.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)
- self._config["features.graph.bw.accounting.rate"] = max(1, self._config["features.graph.bw.accounting.rate"])
-
- # 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.TOR_INIT) # initializes values
- conn.addStatusListener(self.resetListener)
-
- 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 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/state" % 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
- if time.localtime()[8]: tz_offset = time.altzone
- else: tz_offset = time.timezone
-
- for line in stateFile:
- line = line.strip()
-
- if line.startswith("BWHistoryReadValues"):
- bwReadEntries = line[20:].split(",")
- bwReadEntries = [int(entry) / 1024.0 / 900 for entry in bwReadEntries]
- elif line.startswith("BWHistoryWriteValues"):
- bwWriteEntries = line[21:].split(",")
- bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
- elif line.startswith("BWHistoryReadEnds"):
- lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
- 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
- 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.primaryTotal += readVal * 900
- self.secondaryTotal += writeVal * 900
- self.tick += 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):
- # if display is narrow, overwrites x-axis labels with avg / total stats
- if width <= COLLAPSE_WIDTH:
- # clears line
- panel.addstr(8, 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(8, 1, primaryFooter, uiTools.getColor(self.getColor(True)))
- panel.addstr(8, 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(10, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
-
- resetTime = self.accountingInfo["resetTime"]
- if not resetTime: resetTime = "unknown"
- panel.addstr(10, 35, "Time to reset: %s" % resetTime)
-
- used, total = self.accountingInfo["read"], self.accountingInfo["readLimit"]
- if used and total:
- panel.addstr(11, 2, "%s / %s" % (used, total), uiTools.getColor(self.getColor(True)))
-
- used, total = self.accountingInfo["written"], self.accountingInfo["writtenLimit"]
- if used and total:
- panel.addstr(11, 37, "%s / %s" % (used, total), uiTools.getColor(self.getColor(False)))
- else:
- panel.addfstr(10, 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 = "Downloaded" if isPrimary else "Uploaded"
- 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))
-
- # 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 getPreferredHeight(self):
- return 13 if self.isAccounting else 10
-
- 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.getMyFingerprint()
- 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()
-
- if bwRate and bwBurst:
- bwRateLabel = uiTools.getSizeLabel(bwRate, 1)
- bwBurstLabel = uiTools.getSizeLabel(bwBurst, 1)
-
- # 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" % bwRateLabel)
- stats.append("burst: %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" % uiTools.getSizeLabel(bwObserved, 1))
- elif bwMeasured:
- stats.append("measured: %s" % uiTools.getSizeLabel(bwMeasured, 1))
-
- self._titleStats = stats
-
- def _getAvgLabel(self, isPrimary):
- total = self.primaryTotal if isPrimary else self.secondaryTotal
- return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick)) * 1024, 1)
-
- def _getTotalLabel(self, isPrimary):
- total = self.primaryTotal if isPrimary else self.secondaryTotal
- 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()
-
Copied: arm/trunk/src/interface/graphing/bandwidthStats.py (from rev 22998, arm/trunk/interface/graphing/bandwidthStats.py)
===================================================================
--- arm/trunk/src/interface/graphing/bandwidthStats.py (rev 0)
+++ arm/trunk/src/interface/graphing/bandwidthStats.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,368 @@
+"""
+Tracks bandwidth usage of the tor process, expanding to include accounting
+stats if they're set.
+"""
+
+import time
+
+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.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)
+ self._config["features.graph.bw.accounting.rate"] = max(1, self._config["features.graph.bw.accounting.rate"])
+
+ # 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.TOR_INIT) # initializes values
+ conn.addStatusListener(self.resetListener)
+
+ 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.TOR_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/state" % 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
+ if time.localtime()[8]: tz_offset = time.altzone
+ else: tz_offset = 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.primaryTotal += readVal * 900
+ self.secondaryTotal += writeVal * 900
+ self.tick += 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 = "Downloaded" if isPrimary else "Uploaded"
+ 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))
+
+ # 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.getMyFingerprint()
+ 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()
+
+ if bwRate and bwBurst:
+ bwRateLabel = uiTools.getSizeLabel(bwRate, 1)
+ bwBurstLabel = uiTools.getSizeLabel(bwBurst, 1)
+
+ # 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" % bwRateLabel)
+ stats.append("burst: %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" % uiTools.getSizeLabel(bwObserved, 1))
+ elif bwMeasured:
+ stats.append("measured: %s" % uiTools.getSizeLabel(bwMeasured, 1))
+
+ self._titleStats = stats
+
+ def _getAvgLabel(self, isPrimary):
+ total = self.primaryTotal if isPrimary else self.secondaryTotal
+ return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick)) * 1024, 1)
+
+ def _getTotalLabel(self, isPrimary):
+ total = self.primaryTotal if isPrimary else self.secondaryTotal
+ 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()
+
Deleted: arm/trunk/src/interface/graphing/graphPanel.py
===================================================================
--- arm/trunk/interface/graphing/graphPanel.py 2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/graphing/graphPanel.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,363 +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 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_HEIGHT = 10 # space needed for graph and content
-DEFAULT_COLOR_PRIMARY, DEFAULT_COLOR_SECONDARY = "green", "cyan"
-
-# 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_GLOBAL_MAX, BOUNDS_LOCAL_MAX, BOUNDS_TIGHT = range(3)
-BOUND_LABELS = {BOUNDS_GLOBAL_MAX: "global max", BOUNDS_LOCAL_MAX: "local max", BOUNDS_TIGHT: "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.interval": 0, "features.graph.bound": 1, "features.graph.maxSize": 150, "features.graph.frequentRefresh": True}
-
-def loadConfig(config):
- config.update(CONFIG)
- CONFIG["features.graph.interval"] = max(len(UPDATE_INTERVALS) - 1, min(0, CONFIG["features.graph.interval"]))
- CONFIG["features.graph.bound"] = max(2, min(0, CONFIG["features.graph.bound"]))
- CONFIG["features.graph.maxSize"] = max(CONFIG["features.graph.maxSize"], 1)
-
-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.maxSize"]
- 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:
- if CONFIG["features.graph.frequentRefresh"]: return True
- else:
- updateRate = UPDATE_INTERVALS[self._graphPanel.updateInterval][1]
- if (self.tick + 1) % updateRate == 0: return True
-
- 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 getPreferredHeight(self):
- """
- Provides the height content should take up. By default this provides the
- space needed for the default graph and content.
- """
-
- return DEFAULT_HEIGHT
-
- 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 = CONFIG["features.graph.bound"]
- 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:
- return self.stats[self.currentDisplay].getPreferredHeight()
- else: return 0
-
- def draw(self, subwindow, 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 = param.maxPrimary[self.updateInterval]
- secondaryMaxBound = 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 = max(param.primaryCounts[self.updateInterval][1:graphCol + 1])
- secondaryMaxBound = max(param.secondaryCounts[self.updateInterval][1:graphCol + 1])
-
- primaryMinBound = secondaryMinBound = 0
- if self.bounds == BOUNDS_TIGHT:
- primaryMinBound = min(param.primaryCounts[self.updateInterval][1:graphCol + 1])
- secondaryMinBound = 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 bound
- self.addstr(2, 0, "%4i" % primaryMaxBound, primaryColor)
- self.addstr(7, 0, "%4i" % primaryMinBound, primaryColor)
-
- self.addstr(2, graphCol + 5, "%4i" % secondaryMaxBound, secondaryColor)
- self.addstr(7, graphCol + 5, "%4i" % secondaryMinBound, secondaryColor)
-
- # creates bar graph (both primary and secondary)
- for col in range(graphCol):
- colCount = param.primaryCounts[self.updateInterval][col + 1] - primaryMinBound
- colHeight = min(5, 5 * colCount / (max(1, primaryMaxBound) - primaryMinBound))
- for row in range(colHeight): self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
-
- colCount = param.secondaryCounts[self.updateInterval][col + 1] - secondaryMinBound
- colHeight = min(5, 5 * colCount / (max(1, secondaryMaxBound) - secondaryMinBound))
- for row in range(colHeight): self.addstr(7 - 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(8, 4 + loc, timeLabel, primaryColor)
- self.addstr(8, 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)
-
Copied: arm/trunk/src/interface/graphing/graphPanel.py (from rev 22991, arm/trunk/interface/graphing/graphPanel.py)
===================================================================
--- arm/trunk/src/interface/graphing/graphPanel.py (rev 0)
+++ arm/trunk/src/interface/graphing/graphPanel.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,383 @@
+"""
+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 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_GLOBAL_MAX, BOUNDS_LOCAL_MAX, BOUNDS_TIGHT = range(3)
+BOUND_LABELS = {BOUNDS_GLOBAL_MAX: "global max", BOUNDS_LOCAL_MAX: "local max", BOUNDS_TIGHT: "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": 5, "features.graph.interval": 0, "features.graph.bound": 1, "features.graph.maxWidth": 150, "features.graph.frequentRefresh": True}
+
+def loadConfig(config):
+ config.update(CONFIG)
+ CONFIG["features.graph.height"] = max(MIN_GRAPH_HEIGHT, CONFIG["features.graph.height"])
+ CONFIG["features.graph.maxWidth"] = max(1, CONFIG["features.graph.maxWidth"])
+ CONFIG["features.graph.interval"] = min(len(UPDATE_INTERVALS) - 1, max(0, CONFIG["features.graph.interval"]))
+ CONFIG["features.graph.bound"] = min(2, max(0, CONFIG["features.graph.bound"]))
+
+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:
+ if CONFIG["features.graph.frequentRefresh"]: return True
+ else:
+ updateRate = UPDATE_INTERVALS[self._graphPanel.updateInterval][1]
+ if (self.tick + 1) % updateRate == 0: return True
+
+ 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 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 = 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, subwindow, 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 = param.maxPrimary[self.updateInterval]
+ secondaryMaxBound = 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 = max(param.primaryCounts[self.updateInterval][1:graphCol + 1])
+ secondaryMaxBound = max(param.secondaryCounts[self.updateInterval][1:graphCol + 1])
+
+ primaryMinBound = secondaryMinBound = 0
+ if self.bounds == BOUNDS_TIGHT:
+ primaryMinBound = min(param.primaryCounts[self.updateInterval][1:graphCol + 1])
+ secondaryMinBound = 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 bound
+ 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)
+
+ # creates bar graph (both primary and secondary)
+ for col in range(graphCol):
+ colCount = 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 = 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)
+
Deleted: arm/trunk/src/interface/graphing/psStats.py
===================================================================
--- arm/trunk/interface/graphing/psStats.py 2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/graphing/psStats.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,129 +0,0 @@
-"""
-Tracks configured ps stats. If non-numeric then this fails, providing a blank
-graph. By default this provides the cpu and memory usage of the tor process.
-"""
-
-import graphPanel
-from util import log, sysTools, torTools, uiTools
-
-# number of subsequent failed queries before giving up
-FAILURE_THRESHOLD = 5
-
-# attempts to use cached results from the header panel's ps calls
-HEADER_PS_PARAM = ["%cpu", "rss", "%mem", "etime"]
-
-DEFAULT_CONFIG = {"features.graph.ps.primaryStat": "%cpu", "features.graph.ps.secondaryStat": "rss", "features.graph.ps.cachedOnly": True, "log.graph.ps.invalidStat": log.WARN, "log.graph.ps.abandon": log.WARN}
-
-class PsStats(graphPanel.GraphStats):
- """
- Tracks ps stats, defaulting to system resource usage (cpu and memory usage).
- """
-
- def __init__(self, config=None):
- graphPanel.GraphStats.__init__(self)
- self.failedCount = 0 # number of subsequent failed queries
-
- self._config = dict(DEFAULT_CONFIG)
- if config: config.update(self._config)
-
- self.queryPid = torTools.getConn().getMyPid()
- self.queryParam = [self._config["features.graph.ps.primaryStat"], self._config["features.graph.ps.secondaryStat"]]
-
- # If we're getting the same stats as the header panel then issues identical
- # queries to make use of cached results. If not, then disable cache usage.
- if self.queryParam[0] in HEADER_PS_PARAM and self.queryParam[1] in HEADER_PS_PARAM:
- self.queryParam = list(HEADER_PS_PARAM)
- else: self._config["features.graph.ps.cachedOnly"] = False
-
- # strips any empty entries
- while "" in self.queryParam: self.queryParam.remove("")
-
- self.cacheTime = 3600 if self._config["features.graph.ps.cachedOnly"] else 1
-
- 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: statName = self._config["features.graph.ps.primaryStat"]
- else: statName = self._config["features.graph.ps.secondaryStat"]
-
- # provides nice labels for failures and common stats
- if not statName or self.failedCount >= FAILURE_THRESHOLD or not statName in self.queryParam:
- return ""
- elif statName == "%cpu":
- return "CPU (%s%%, avg: %0.1f%%):" % (lastAmount, avg)
- elif statName in ("rss", "size"):
- # memory sizes are converted from MB to B before generating labels
- statLabel = "Memory" if statName == "rss" else "Size"
- usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
- avgLabel = uiTools.getSizeLabel(avg * 1048576, 1)
- return "%s (%s, avg: %s):" % (statLabel, usageLabel, avgLabel)
- else:
- # generic label (first letter of stat name is capitalized)
- statLabel = statName[0].upper() + statName[1:]
- return "%s (%s, avg: %s):" % (statLabel, lastAmount, avg)
-
- def getPreferredHeight(self):
- # hides graph if there's nothing to display (provides default otherwise)
- # provides default height unless there's nothing to
- if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
- return graphPanel.DEFAULT_HEIGHT
- else: return 0
-
- def eventTick(self):
- """
- Processes a ps event.
- """
-
- psResults = {} # mapping of stat names to their results
- if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
- queryCmd = "ps -p %s -o %s" % (self.queryPid, ",".join(self.queryParam))
- psCall = sysTools.call(queryCmd, self.cacheTime, True)
-
- if psCall and len(psCall) == 2:
- # ps provided results (first line is headers, second is stats)
- stats = psCall[1].strip().split()
-
- if len(self.queryParam) == len(stats):
- # we have a result to match each stat - constructs mapping
- psResults = dict([(self.queryParam[i], stats[i]) for i in range(len(stats))])
- self.failedCount = 0 # had a successful call - reset failure count
-
- if not psResults:
- # ps call failed, if we fail too many times sequentially then abandon
- # listing (probably due to invalid ps parameters)
- self.failedCount += 1
-
- if self.failedCount == FAILURE_THRESHOLD:
- msg = "failed several attempts to query '%s', abandoning ps graph" % queryCmd
- log.log(self._config["log.graph.ps.abandon"], msg)
-
- # if something fails (no pid, ps call failed, etc) then uses last results
- primary, secondary = self.lastPrimary, self.lastSecondary
-
- for isPrimary in (True, False):
- if isPrimary: statName = self._config["features.graph.ps.primaryStat"]
- else: statName = self._config["features.graph.ps.secondaryStat"]
-
- if statName in psResults:
- try:
- result = float(psResults[statName])
-
- # The 'rss' and 'size' parameters provide memory usage in KB. This is
- # scaled up to MB so the graph's y-high is a reasonable value.
- if statName in ("rss", "size"): result /= 1024.0
-
- if isPrimary: primary = result
- else: secondary = result
- except ValueError:
- if self.queryParam != HEADER_PS_PARAM:
- # custom stat provides non-numeric results - give a warning and stop querying it
- msg = "unable to use non-numeric ps stat '%s' for graphing" % statName
- log.log(self._config["log.graph.ps.invalidStat"], msg)
- self.queryParam.remove(statName)
-
- self._processEvent(primary, secondary)
-
Copied: arm/trunk/src/interface/graphing/psStats.py (from rev 22952, arm/trunk/interface/graphing/psStats.py)
===================================================================
--- arm/trunk/src/interface/graphing/psStats.py (rev 0)
+++ arm/trunk/src/interface/graphing/psStats.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,131 @@
+"""
+Tracks configured ps stats. If non-numeric then this fails, providing a blank
+graph. By default this provides the cpu and memory usage of the tor process.
+"""
+
+import graphPanel
+from util import log, sysTools, torTools, uiTools
+
+# number of subsequent failed queries before giving up
+FAILURE_THRESHOLD = 5
+
+# attempts to use cached results from the header panel's ps calls
+HEADER_PS_PARAM = ["%cpu", "rss", "%mem", "etime"]
+
+DEFAULT_CONFIG = {"features.graph.ps.primaryStat": "%cpu", "features.graph.ps.secondaryStat": "rss", "features.graph.ps.cachedOnly": True, "log.graph.ps.invalidStat": log.WARN, "log.graph.ps.abandon": log.WARN}
+
+class PsStats(graphPanel.GraphStats):
+ """
+ Tracks ps stats, defaulting to system resource usage (cpu and memory usage).
+ """
+
+ def __init__(self, config=None):
+ graphPanel.GraphStats.__init__(self)
+ self.failedCount = 0 # number of subsequent failed queries
+
+ self._config = dict(DEFAULT_CONFIG)
+ if config: config.update(self._config)
+
+ self.queryPid = torTools.getConn().getMyPid()
+ self.queryParam = [self._config["features.graph.ps.primaryStat"], self._config["features.graph.ps.secondaryStat"]]
+
+ # If we're getting the same stats as the header panel then issues identical
+ # queries to make use of cached results. If not, then disable cache usage.
+ if self.queryParam[0] in HEADER_PS_PARAM and self.queryParam[1] in HEADER_PS_PARAM:
+ self.queryParam = list(HEADER_PS_PARAM)
+ else: self._config["features.graph.ps.cachedOnly"] = False
+
+ # strips any empty entries
+ while "" in self.queryParam: self.queryParam.remove("")
+
+ self.cacheTime = 3600 if self._config["features.graph.ps.cachedOnly"] else 1
+
+ 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: statName = self._config["features.graph.ps.primaryStat"]
+ else: statName = self._config["features.graph.ps.secondaryStat"]
+
+ # provides nice labels for failures and common stats
+ if not statName or self.failedCount >= FAILURE_THRESHOLD or not statName in self.queryParam:
+ return ""
+ elif statName == "%cpu":
+ return "CPU (%s%%, avg: %0.1f%%):" % (lastAmount, avg)
+ elif statName in ("rss", "size"):
+ # memory sizes are converted from MB to B before generating labels
+ statLabel = "Memory" if statName == "rss" else "Size"
+ usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
+ avgLabel = uiTools.getSizeLabel(avg * 1048576, 1)
+ return "%s (%s, avg: %s):" % (statLabel, usageLabel, avgLabel)
+ else:
+ # generic label (first letter of stat name is capitalized)
+ statLabel = statName[0].upper() + statName[1:]
+ return "%s (%s, avg: %s):" % (statLabel, lastAmount, avg)
+
+ def isVisible(self):
+ """
+ Hides graph if unable to fetch stats.
+ """
+
+ if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
+ return graphPanel.GraphStats.isVisible(self)
+ else: return False
+
+ def eventTick(self):
+ """
+ Processes a ps event.
+ """
+
+ psResults = {} # mapping of stat names to their results
+ if self.queryPid and self.queryParam and self.failedCount < FAILURE_THRESHOLD:
+ queryCmd = "ps -p %s -o %s" % (self.queryPid, ",".join(self.queryParam))
+ psCall = sysTools.call(queryCmd, self.cacheTime, True)
+
+ if psCall and len(psCall) == 2:
+ # ps provided results (first line is headers, second is stats)
+ stats = psCall[1].strip().split()
+
+ if len(self.queryParam) == len(stats):
+ # we have a result to match each stat - constructs mapping
+ psResults = dict([(self.queryParam[i], stats[i]) for i in range(len(stats))])
+ self.failedCount = 0 # had a successful call - reset failure count
+
+ if not psResults:
+ # ps call failed, if we fail too many times sequentially then abandon
+ # listing (probably due to invalid ps parameters)
+ self.failedCount += 1
+
+ if self.failedCount == FAILURE_THRESHOLD:
+ msg = "failed several attempts to query '%s', abandoning ps graph" % queryCmd
+ log.log(self._config["log.graph.ps.abandon"], msg)
+
+ # if something fails (no pid, ps call failed, etc) then uses last results
+ primary, secondary = self.lastPrimary, self.lastSecondary
+
+ for isPrimary in (True, False):
+ if isPrimary: statName = self._config["features.graph.ps.primaryStat"]
+ else: statName = self._config["features.graph.ps.secondaryStat"]
+
+ if statName in psResults:
+ try:
+ result = float(psResults[statName])
+
+ # The 'rss' and 'size' parameters provide memory usage in KB. This is
+ # scaled up to MB so the graph's y-high is a reasonable value.
+ if statName in ("rss", "size"): result /= 1024.0
+
+ if isPrimary: primary = result
+ else: secondary = result
+ except ValueError:
+ if self.queryParam != HEADER_PS_PARAM:
+ # custom stat provides non-numeric results - give a warning and stop querying it
+ msg = "unable to use non-numeric ps stat '%s' for graphing" % statName
+ log.log(self._config["log.graph.ps.invalidStat"], msg)
+ self.queryParam.remove(statName)
+
+ self._processEvent(primary, secondary)
+
Deleted: arm/trunk/src/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py 2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/interface/logPanel.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,485 +0,0 @@
-#!/usr/bin/env python
-# logPanel.py -- Resources related to Tor event monitoring.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-import curses
-from curses.ascii import isprint
-from TorCtl import TorCtl
-
-from util import log, panel, sysTools, torTools, uiTools
-
-PRE_POPULATE_LOG = True # attempts to retrieve events from log file if available
-
-# truncates to the last X log lines (needed to start in a decent time if the log's big)
-PRE_POPULATE_MIN_LIMIT = 1000 # limit in case of verbose logging
-PRE_POPULATE_MAX_LIMIT = 5000 # limit for NOTICE - ERR (since most lines are skipped)
-MAX_LOG_ENTRIES = 1000 # size of log buffer (max number of entries)
-RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
-
-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"""
-
-TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
-
-def expandEvents(eventAbbr):
- """
- Expands event abbreviations to their full names. Beside mappings privided 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" -> []
- """
-
- expandedEvents = set()
- invalidFlags = ""
- for flag in eventAbbr:
- if flag == "A":
- expandedEvents = set(TOR_EVENT_TYPES.values() + ["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"])
- break
- elif flag == "X":
- expandedEvents = set()
- break
- elif flag == "U": expandedEvents.add("UNKNOWN")
- elif flag == "D": expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
- elif flag == "I": expandedEvents = expandedEvents.union(set(["INFO", "NOTICE", "WARN", "ERR"]))
- elif flag == "N": expandedEvents = expandedEvents.union(set(["NOTICE", "WARN", "ERR"]))
- elif flag == "W": expandedEvents = expandedEvents.union(set(["WARN", "ERR"]))
- elif flag == "E": expandedEvents.add("ERR")
- elif flag == "1": expandedEvents = expandedEvents.union(set(["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
- elif flag == "2": expandedEvents = expandedEvents.union(set(["ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
- elif flag == "3": expandedEvents = expandedEvents.union(set(["ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
- elif flag == "4": expandedEvents = expandedEvents.union(set(["ARM_WARN", "ARM_ERR"]))
- elif flag == "5": expandedEvents.add("ARM_ERR")
- elif flag == "6": expandedEvents = expandedEvents.union(set(["TORCTL_DEBUG", "TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
- elif flag == "7": expandedEvents = expandedEvents.union(set(["TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
- elif flag == "8": expandedEvents = expandedEvents.union(set(["TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
- elif flag == "9": expandedEvents = expandedEvents.union(set(["TORCTL_WARN", "TORCTL_ERR"]))
- elif flag == "0": expandedEvents.add("TORCTL_ERR")
- elif flag in TOR_EVENT_TYPES:
- expandedEvents.add(TOR_EVENT_TYPES[flag])
- else:
- invalidFlags += flag
-
- if invalidFlags: raise ValueError(invalidFlags)
- else: return expandedEvents
-
-class LogMonitor(TorCtl.PostEventListener, panel.Panel):
- """
- Tor event listener, noting messages, the time, and their type in a panel.
- """
-
- def __init__(self, stdscr, conn, loggedEvents):
- TorCtl.PostEventListener.__init__(self)
- panel.Panel.__init__(self, stdscr, "log", 0)
- self.scroll = 0
- self.msgLog = [] # tuples of (logText, color)
- self.isPaused = False
- self.pauseBuffer = [] # location where messages are buffered if paused
- self.loggedEvents = loggedEvents # events we're listening to
- self.lastHeartbeat = time.time() # time of last event
- self.regexFilter = None # filter for presented log events (no filtering if None)
- self.eventTimeOverwrite = None # replaces time for further events with this (uses time it occures if None)
- self.controlPortClosed = False # flag set if TorCtl provided notice that control port is closed
-
- # prevents attempts to redraw while processing batch of events
- previousPauseState = self.isPaused
- self.setPaused(True)
- log.addListeners([log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR], self.arm_event_wrapper, True)
- self.setPaused(previousPauseState)
-
- # attempts to process events from log file
- if PRE_POPULATE_LOG:
- previousPauseState = self.isPaused
-
- try:
- logFileLoc = None
- loggingLocations = conn.get_option("Log")
-
- for entry in loggingLocations:
- entryComp = entry[1].split()
- if entryComp[1] == "file":
- logFileLoc = entryComp[2]
- break
-
- if logFileLoc:
- # prevents attempts to redraw while processing batch of events
- self.setPaused(True)
-
- # trims log to last entries to deal with logs when they're in the GB or TB range
- # throws IOError if tail fails (falls to the catch-all later)
- # TODO: now that this is using sysTools figure out if we can do away with the catch-all...
- limit = PRE_POPULATE_MIN_LIMIT if ("DEBUG" in self.loggedEvents or "INFO" in self.loggedEvents) else PRE_POPULATE_MAX_LIMIT
-
- # truncates to entries for this tor instance
- lines = sysTools.call("tail -n %i %s" % (limit, logFileLoc))
- instanceStart = 0
- for i in range(len(lines) - 1, -1, -1):
- if "opening log file" in lines[i]:
- instanceStart = i
- break
-
- for line in lines[instanceStart:]:
- lineComp = line.split()
- eventType = lineComp[3][1:-1].upper()
-
- if eventType in self.loggedEvents:
- timeComp = lineComp[2][:lineComp[2].find(".")].split(":")
- self.eventTimeOverwrite = (0, 0, 0, int(timeComp[0]), int(timeComp[1]), int(timeComp[2]))
- self.listen(TorCtl.LogEvent(eventType, " ".join(lineComp[4:])))
- except Exception: pass # disreguard any issues that might arise
- finally:
- self.setPaused(previousPauseState)
- self.eventTimeOverwrite = None
-
- def handleKey(self, key):
- # scroll movement
- if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
- pageHeight, shift = self.getPreferredSize()[0] - 1, 0
-
- # location offset
- if key == curses.KEY_UP: shift = -1
- elif key == curses.KEY_DOWN: shift = 1
- elif key == curses.KEY_PPAGE: shift = -pageHeight
- elif key == curses.KEY_NPAGE: shift = pageHeight
-
- # restricts to valid bounds and applies
- maxLoc = self.getLogDisplayLength() - pageHeight
- self.scroll = max(0, min(self.scroll + shift, maxLoc))
-
- # Listens for all event types and redirects to registerEvent
- def circ_status_event(self, event):
- if "CIRC" in self.loggedEvents:
- optionalParams = ""
- if event.purpose: optionalParams += " PURPOSE: %s" % event.purpose
- if event.reason: optionalParams += " REASON: %s" % event.reason
- if event.remote_reason: optionalParams += " REMOTE_REASON: %s" % event.remote_reason
- self.registerEvent("CIRC", "ID: %-3s STATUS: %-10s PATH: %s%s" % (event.circ_id, event.status, ", ".join(event.path), optionalParams), "yellow")
-
- def buildtimeout_set_event(self, event):
- # TODO: not sure how to stimulate event - needs sanity check
- try:
- self.registerEvent("BUILDTIMEOUT_SET", "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), "white")
- except TypeError:
- self.registerEvent("BUILDTIMEOUT_SET", "DEBUG -> SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (type(event.set_type), type(event.total_times), type(event.timeout_ms), type(event.xm), type(event.alpha), type(event.cutoff_quantile)), "white")
-
- def stream_status_event(self, event):
- # TODO: not sure how to stimulate event - needs sanity check
- try:
- self.registerEvent("STREAM", "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), "white")
- except TypeError:
- self.registerEvent("STREAM", "DEBUG -> ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (type(event.strm_id), type(event.status), type(event.circ_id), type(event.target_host), type(event.target_port), type(event.reason), type(event.remote_reason), type(event.source), type(event.source_addr), type(event.purpose)), "white")
-
- def or_conn_status_event(self, event):
- optionalParams = ""
- if event.age: optionalParams += " AGE: %-3s" % event.age
- if event.read_bytes: optionalParams += " READ: %-4i" % event.read_bytes
- if event.wrote_bytes: optionalParams += " WRITTEN: %-4i" % event.wrote_bytes
- if event.reason: optionalParams += " REASON: %-6s" % event.reason
- if event.ncircs: optionalParams += " NCIRCS: %i" % event.ncircs
- self.registerEvent("ORCONN", "STATUS: %-10s ENDPOINT: %-20s%s" % (event.status, event.endpoint, optionalParams), "white")
-
- def stream_bw_event(self, event):
- # TODO: not sure how to stimulate event - needs sanity check
- try:
- self.registerEvent("STREAM_BW", "ID: %s READ: %i WRITTEN: %i" % (event.strm_id, event.bytes_read, event.bytes_written), "white")
- except TypeError:
- self.registerEvent("STREAM_BW", "DEBUG -> ID: %s READ: %s WRITTEN: %s" % (type(event.strm_id), type(event.bytes_read), type(event.bytes_written)), "white")
-
- def bandwidth_event(self, event):
- self.lastHeartbeat = time.time() # ensures heartbeat at least once a second
- if "BW" in self.loggedEvents: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
-
- def msg_event(self, event):
- self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
-
- def new_desc_event(self, event):
- if "NEWDESC" in self.loggedEvents:
- idlistStr = [str(item) for item in event.idlist]
- self.registerEvent("NEWDESC", ", ".join(idlistStr), "white")
-
- def address_mapped_event(self, event):
- self.registerEvent("ADDRMAP", "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr), "white")
-
- def ns_event(self, event):
- # NetworkStatus params: nickname, idhash, orhash, ip, orport (int), dirport (int), flags, idhex, bandwidth, updated (datetime)
- if "NS" in self.loggedEvents:
- msg = ""
- for ns in event.nslist:
- msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
- if len(msg) > 1: msg = msg[2:]
- self.registerEvent("NS", "Listed (%i): %s" % (len(event.nslist), msg), "blue")
-
- def new_consensus_event(self, event):
- if "NEWCONSENSUS" in self.loggedEvents:
- msg = ""
- for ns in event.nslist:
- msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
- self.registerEvent("NEWCONSENSUS", "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
-
- def unknown_event(self, event):
- if "UNKNOWN" in self.loggedEvents: self.registerEvent("UNKNOWN", event.event_string, "red")
-
- def arm_event_wrapper(self, level, msg, eventTime):
- # temporary adaptor hack to use the new logging functions until I'm sure they'll work
- # TODO: insert into log according to the event's timestamp (harder part
- # here will be interpreting tor's event timestamps...)
- self.monitor_event(level, msg)
-
- def monitor_event(self, level, msg):
- # events provided by the arm monitor
- if "ARM_" + level in self.loggedEvents: self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
-
- def tor_ctl_event(self, level, msg):
- # events provided by TorCtl
- if "TORCTL_" + level in self.loggedEvents: self.registerEvent("TORCTL-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
-
- def write(self, msg):
- """
- Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
- """
-
- timestampStart = msg.find("[")
- timestampEnd = msg.find("]")
-
- level = msg[:timestampStart]
- msg = msg[timestampEnd + 2:].strip()
-
- if TOR_CTL_CLOSE_MSG in msg:
- # TorCtl providing notice that control port is closed
- self.controlPortClosed = True
- log.log(log.NOTICE, "Tor control port closed")
-
- # Allows the Controller to notice that tor's shut down.
- # TODO: should make the controller the torctl event listener rather than
- # this log panel (it'll also make this less hacky)
- torTools.getConn().isAlive()
- self.tor_ctl_event(level, msg)
-
- def flush(self): pass
-
- def registerEvent(self, type, msg, color):
- """
- Notes event and redraws log. If paused it's held in a temporary buffer. If
- msg is a list then this is expanded to multiple lines.
- """
-
- if not type.startswith("ARM"): self.lastHeartbeat = time.time()
- eventTime = self.eventTimeOverwrite if self.eventTimeOverwrite else time.localtime()
- toAdd = []
-
- # wraps if a single line message
- if isinstance(msg, str): msg = [msg]
-
- firstLine = True
- for msgLine in msg:
- # strips control characters to avoid screwing up the terminal
- msgLine = "".join([char for char in msgLine if isprint(char)])
-
- header = "%02i:%02i:%02i %s" % (eventTime[3], eventTime[4], eventTime[5], "[%s]" % type) if firstLine else ""
- toAdd.append("%s %s" % (header, msgLine))
- firstLine = False
-
- toAdd.reverse()
- if self.isPaused:
- for msgLine in toAdd: self.pauseBuffer.insert(0, (msgLine, color))
- if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
- else:
- for msgLine in toAdd: self.msgLog.insert(0, (msgLine, color))
- if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
- self.redraw(True)
-
- def draw(self, subwindow, width, height):
- """
- Redraws message log. Entries stretch to use available space and may
- contain up to two lines. Starts with newest entries.
- """
-
- isScrollBarVisible = self.getLogDisplayLength() > height - 1
- xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
-
- # draws label - uses ellipsis if too long, for instance:
- # Events (DEBUG, INFO, NOTICE, WARN...):
- eventsLabel = "Events"
-
- # separates tor and arm runlevels (might be able to show as range)
- eventsList = list(self.loggedEvents)
- torRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, ""))
- armRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "ARM_"))
- torctlRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "TORCTL_"))
-
- if torctlRunlevelLabel: eventsList = ["TORCTL " + torctlRunlevelLabel] + eventsList
- if armRunlevelLabel: eventsList = ["ARM " + armRunlevelLabel] + eventsList
- if torRunlevelLabel: eventsList = [torRunlevelLabel] + eventsList
-
- eventsListing = ", ".join(eventsList)
- filterLabel = "" if not self.regexFilter else " - filter: %s" % self.regexFilter.pattern
-
- firstLabelLen = eventsListing.find(", ")
- if firstLabelLen == -1: firstLabelLen = len(eventsListing)
- else: firstLabelLen += 3
-
- if width > 10 + firstLabelLen:
- eventsLabel += " ("
-
- if len(eventsListing) > width - 11:
- labelBreak = eventsListing[:width - 12].rfind(", ")
- eventsLabel += "%s..." % eventsListing[:labelBreak]
- elif len(eventsListing) + len(filterLabel) > width - 11:
- eventsLabel += eventsListing
- else: eventsLabel += eventsListing + filterLabel
- eventsLabel += ")"
- eventsLabel += ":"
-
- self.addstr(0, 0, eventsLabel, curses.A_STANDOUT)
-
- # log entries
- maxLoc = self.getLogDisplayLength() - height + 1
- self.scroll = max(0, min(self.scroll, maxLoc))
- lineCount = 1 - self.scroll
-
- for (line, color) in self.msgLog:
- if self.regexFilter and not self.regexFilter.search(line):
- continue # filter doesn't match log message - skip
-
- # splits over too lines if too long
- if len(line) < width:
- if lineCount >= 1: self.addstr(lineCount, xOffset, line, uiTools.getColor(color))
- lineCount += 1
- else:
- (line1, line2) = splitLine(line, width - xOffset)
- if lineCount >= 1: self.addstr(lineCount, xOffset, line1, uiTools.getColor(color))
- if lineCount >= 0: self.addstr(lineCount + 1, xOffset, line2, uiTools.getColor(color))
- lineCount += 2
-
- if lineCount >= height: break # further log messages wouldn't fit
-
- if isScrollBarVisible: self.addScrollBar(self.scroll, self.scroll + height - 1, self.getLogDisplayLength(), 1)
-
- def getLogDisplayLength(self):
- """
- Provides the number of lines the log would currently occupy.
- """
-
- logLength = len(self.msgLog)
-
- # takes into account filtered and wrapped messages
- for (line, color) in self.msgLog:
- if self.regexFilter and not self.regexFilter.search(line): logLength -= 1
- elif len(line) >= self.getPreferredSize()[1]: logLength += 1
-
- return logLength
-
- 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.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
- if self.win: self.redraw(True) # hack to avoid redrawing during init
-
- def getHeartbeat(self):
- """
- Provides the number of seconds since the last registered event (this always
- listens to BW events so should be less than a second if relay's still
- responsive).
- """
-
- return time.time() - self.lastHeartbeat
-
-def parseRunlevelRanges(eventsList, searchPrefix):
- """
- This parses a list of events to provide an ordered list of runlevels,
- condensed if three or more are in a contiguous range. This removes parsed
- runlevels from the eventsList. For instance:
-
- eventsList = ["BW", "ARM_WARN", "ERR", "ARM_ERR", "ARM_DEBUG", "ARM_NOTICE"]
- searchPrefix = "ARM_"
-
- results in:
- eventsList = ["BW", "ERR"]
- return value is ["DEBUG", "NOTICE - ERR"]
-
- """
-
- # blank ending runlevel forces the break condition to be reached at the end
- runlevels = ["DEBUG", "INFO", "NOTICE", "WARN", "ERR", ""]
- runlevelLabels = []
- start, end = "", ""
- rangeLength = 0
-
- for level in runlevels:
- if searchPrefix + level in eventsList:
- eventsList.remove(searchPrefix + level)
-
- if start:
- end = level
- rangeLength += 1
- else:
- start = level
- rangeLength = 1
- elif rangeLength > 0:
- # reached a break in the runlevels
- if rangeLength == 1: runlevelLabels += [start]
- elif rangeLength == 2: runlevelLabels += [start, end]
- else: runlevelLabels += ["%s - %s" % (start, end)]
-
- start, end = "", ""
- rangeLength = 0
-
- return runlevelLabels
-
-def splitLine(message, x):
- """
- Divides message into two lines, attempting to do it on a wordbreak.
- """
-
- lastWordbreak = message[:x].rfind(" ")
- if x - lastWordbreak < 10:
- line1 = message[:lastWordbreak]
- line2 = " %s" % message[lastWordbreak:].strip()
- else:
- # over ten characters until the last word - dividing
- line1 = "%s-" % message[:x - 2]
- line2 = " %s" % message[x - 2:].strip()
-
- # ends line with ellipsis if too long
- if len(line2) > x:
- lastWordbreak = line2[:x - 4].rfind(" ")
-
- # doesn't use wordbreak if it's a long word or the whole line is one
- # word (picking up on two space indent to have index 1)
- if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
- line2 = "%s..." % line2[:lastWordbreak]
-
- return (line1, line2)
-
Copied: arm/trunk/src/interface/logPanel.py (from rev 22948, arm/trunk/interface/logPanel.py)
===================================================================
--- arm/trunk/src/interface/logPanel.py (rev 0)
+++ arm/trunk/src/interface/logPanel.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,485 @@
+#!/usr/bin/env python
+# logPanel.py -- Resources related to Tor event monitoring.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import time
+import curses
+from curses.ascii import isprint
+from TorCtl import TorCtl
+
+from util import log, panel, sysTools, torTools, uiTools
+
+PRE_POPULATE_LOG = True # attempts to retrieve events from log file if available
+
+# truncates to the last X log lines (needed to start in a decent time if the log's big)
+PRE_POPULATE_MIN_LIMIT = 1000 # limit in case of verbose logging
+PRE_POPULATE_MAX_LIMIT = 5000 # limit for NOTICE - ERR (since most lines are skipped)
+MAX_LOG_ENTRIES = 1000 # size of log buffer (max number of entries)
+RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
+
+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"""
+
+TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
+
+def expandEvents(eventAbbr):
+ """
+ Expands event abbreviations to their full names. Beside mappings privided 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" -> []
+ """
+
+ expandedEvents = set()
+ invalidFlags = ""
+ for flag in eventAbbr:
+ if flag == "A":
+ expandedEvents = set(TOR_EVENT_TYPES.values() + ["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"])
+ break
+ elif flag == "X":
+ expandedEvents = set()
+ break
+ elif flag == "U": expandedEvents.add("UNKNOWN")
+ elif flag == "D": expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
+ elif flag == "I": expandedEvents = expandedEvents.union(set(["INFO", "NOTICE", "WARN", "ERR"]))
+ elif flag == "N": expandedEvents = expandedEvents.union(set(["NOTICE", "WARN", "ERR"]))
+ elif flag == "W": expandedEvents = expandedEvents.union(set(["WARN", "ERR"]))
+ elif flag == "E": expandedEvents.add("ERR")
+ elif flag == "1": expandedEvents = expandedEvents.union(set(["ARM_DEBUG", "ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+ elif flag == "2": expandedEvents = expandedEvents.union(set(["ARM_INFO", "ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+ elif flag == "3": expandedEvents = expandedEvents.union(set(["ARM_NOTICE", "ARM_WARN", "ARM_ERR"]))
+ elif flag == "4": expandedEvents = expandedEvents.union(set(["ARM_WARN", "ARM_ERR"]))
+ elif flag == "5": expandedEvents.add("ARM_ERR")
+ elif flag == "6": expandedEvents = expandedEvents.union(set(["TORCTL_DEBUG", "TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
+ elif flag == "7": expandedEvents = expandedEvents.union(set(["TORCTL_INFO", "TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
+ elif flag == "8": expandedEvents = expandedEvents.union(set(["TORCTL_NOTICE", "TORCTL_WARN", "TORCTL_ERR"]))
+ elif flag == "9": expandedEvents = expandedEvents.union(set(["TORCTL_WARN", "TORCTL_ERR"]))
+ elif flag == "0": expandedEvents.add("TORCTL_ERR")
+ elif flag in TOR_EVENT_TYPES:
+ expandedEvents.add(TOR_EVENT_TYPES[flag])
+ else:
+ invalidFlags += flag
+
+ if invalidFlags: raise ValueError(invalidFlags)
+ else: return expandedEvents
+
+class LogMonitor(TorCtl.PostEventListener, panel.Panel):
+ """
+ Tor event listener, noting messages, the time, and their type in a panel.
+ """
+
+ def __init__(self, stdscr, conn, loggedEvents):
+ TorCtl.PostEventListener.__init__(self)
+ panel.Panel.__init__(self, stdscr, "log", 0)
+ self.scroll = 0
+ self.msgLog = [] # tuples of (logText, color)
+ self.isPaused = False
+ self.pauseBuffer = [] # location where messages are buffered if paused
+ self.loggedEvents = loggedEvents # events we're listening to
+ self.lastHeartbeat = time.time() # time of last event
+ self.regexFilter = None # filter for presented log events (no filtering if None)
+ self.eventTimeOverwrite = None # replaces time for further events with this (uses time it occures if None)
+ self.controlPortClosed = False # flag set if TorCtl provided notice that control port is closed
+
+ # prevents attempts to redraw while processing batch of events
+ previousPauseState = self.isPaused
+ self.setPaused(True)
+ log.addListeners([log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR], self.arm_event_wrapper, True)
+ self.setPaused(previousPauseState)
+
+ # attempts to process events from log file
+ if PRE_POPULATE_LOG:
+ previousPauseState = self.isPaused
+
+ try:
+ logFileLoc = None
+ loggingLocations = conn.get_option("Log")
+
+ for entry in loggingLocations:
+ entryComp = entry[1].split()
+ if entryComp[1] == "file":
+ logFileLoc = entryComp[2]
+ break
+
+ if logFileLoc:
+ # prevents attempts to redraw while processing batch of events
+ self.setPaused(True)
+
+ # trims log to last entries to deal with logs when they're in the GB or TB range
+ # throws IOError if tail fails (falls to the catch-all later)
+ # TODO: now that this is using sysTools figure out if we can do away with the catch-all...
+ limit = PRE_POPULATE_MIN_LIMIT if ("DEBUG" in self.loggedEvents or "INFO" in self.loggedEvents) else PRE_POPULATE_MAX_LIMIT
+
+ # truncates to entries for this tor instance
+ lines = sysTools.call("tail -n %i %s" % (limit, logFileLoc))
+ instanceStart = 0
+ for i in range(len(lines) - 1, -1, -1):
+ if "opening log file" in lines[i]:
+ instanceStart = i
+ break
+
+ for line in lines[instanceStart:]:
+ lineComp = line.split()
+ eventType = lineComp[3][1:-1].upper()
+
+ if eventType in self.loggedEvents:
+ timeComp = lineComp[2][:lineComp[2].find(".")].split(":")
+ self.eventTimeOverwrite = (0, 0, 0, int(timeComp[0]), int(timeComp[1]), int(timeComp[2]))
+ self.listen(TorCtl.LogEvent(eventType, " ".join(lineComp[4:])))
+ except Exception: pass # disreguard any issues that might arise
+ finally:
+ self.setPaused(previousPauseState)
+ self.eventTimeOverwrite = None
+
+ def handleKey(self, key):
+ # scroll movement
+ if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
+ pageHeight, shift = self.getPreferredSize()[0] - 1, 0
+
+ # location offset
+ if key == curses.KEY_UP: shift = -1
+ elif key == curses.KEY_DOWN: shift = 1
+ elif key == curses.KEY_PPAGE: shift = -pageHeight
+ elif key == curses.KEY_NPAGE: shift = pageHeight
+
+ # restricts to valid bounds and applies
+ maxLoc = self.getLogDisplayLength() - pageHeight
+ self.scroll = max(0, min(self.scroll + shift, maxLoc))
+
+ # Listens for all event types and redirects to registerEvent
+ def circ_status_event(self, event):
+ if "CIRC" in self.loggedEvents:
+ optionalParams = ""
+ if event.purpose: optionalParams += " PURPOSE: %s" % event.purpose
+ if event.reason: optionalParams += " REASON: %s" % event.reason
+ if event.remote_reason: optionalParams += " REMOTE_REASON: %s" % event.remote_reason
+ self.registerEvent("CIRC", "ID: %-3s STATUS: %-10s PATH: %s%s" % (event.circ_id, event.status, ", ".join(event.path), optionalParams), "yellow")
+
+ def buildtimeout_set_event(self, event):
+ # TODO: not sure how to stimulate event - needs sanity check
+ try:
+ self.registerEvent("BUILDTIMEOUT_SET", "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), "white")
+ except TypeError:
+ self.registerEvent("BUILDTIMEOUT_SET", "DEBUG -> SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (type(event.set_type), type(event.total_times), type(event.timeout_ms), type(event.xm), type(event.alpha), type(event.cutoff_quantile)), "white")
+
+ def stream_status_event(self, event):
+ # TODO: not sure how to stimulate event - needs sanity check
+ try:
+ self.registerEvent("STREAM", "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), "white")
+ except TypeError:
+ self.registerEvent("STREAM", "DEBUG -> ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (type(event.strm_id), type(event.status), type(event.circ_id), type(event.target_host), type(event.target_port), type(event.reason), type(event.remote_reason), type(event.source), type(event.source_addr), type(event.purpose)), "white")
+
+ def or_conn_status_event(self, event):
+ optionalParams = ""
+ if event.age: optionalParams += " AGE: %-3s" % event.age
+ if event.read_bytes: optionalParams += " READ: %-4i" % event.read_bytes
+ if event.wrote_bytes: optionalParams += " WRITTEN: %-4i" % event.wrote_bytes
+ if event.reason: optionalParams += " REASON: %-6s" % event.reason
+ if event.ncircs: optionalParams += " NCIRCS: %i" % event.ncircs
+ self.registerEvent("ORCONN", "STATUS: %-10s ENDPOINT: %-20s%s" % (event.status, event.endpoint, optionalParams), "white")
+
+ def stream_bw_event(self, event):
+ # TODO: not sure how to stimulate event - needs sanity check
+ try:
+ self.registerEvent("STREAM_BW", "ID: %s READ: %i WRITTEN: %i" % (event.strm_id, event.bytes_read, event.bytes_written), "white")
+ except TypeError:
+ self.registerEvent("STREAM_BW", "DEBUG -> ID: %s READ: %s WRITTEN: %s" % (type(event.strm_id), type(event.bytes_read), type(event.bytes_written)), "white")
+
+ def bandwidth_event(self, event):
+ self.lastHeartbeat = time.time() # ensures heartbeat at least once a second
+ if "BW" in self.loggedEvents: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
+
+ def msg_event(self, event):
+ self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
+
+ def new_desc_event(self, event):
+ if "NEWDESC" in self.loggedEvents:
+ idlistStr = [str(item) for item in event.idlist]
+ self.registerEvent("NEWDESC", ", ".join(idlistStr), "white")
+
+ def address_mapped_event(self, event):
+ self.registerEvent("ADDRMAP", "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr), "white")
+
+ def ns_event(self, event):
+ # NetworkStatus params: nickname, idhash, orhash, ip, orport (int), dirport (int), flags, idhex, bandwidth, updated (datetime)
+ if "NS" in self.loggedEvents:
+ msg = ""
+ for ns in event.nslist:
+ msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
+ if len(msg) > 1: msg = msg[2:]
+ self.registerEvent("NS", "Listed (%i): %s" % (len(event.nslist), msg), "blue")
+
+ def new_consensus_event(self, event):
+ if "NEWCONSENSUS" in self.loggedEvents:
+ msg = ""
+ for ns in event.nslist:
+ msg += ", %s (%s:%i)" % (ns.nickname, ns.ip, ns.orport)
+ self.registerEvent("NEWCONSENSUS", "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
+
+ def unknown_event(self, event):
+ if "UNKNOWN" in self.loggedEvents: self.registerEvent("UNKNOWN", event.event_string, "red")
+
+ def arm_event_wrapper(self, level, msg, eventTime):
+ # temporary adaptor hack to use the new logging functions until I'm sure they'll work
+ # TODO: insert into log according to the event's timestamp (harder part
+ # here will be interpreting tor's event timestamps...)
+ self.monitor_event(level, msg)
+
+ def monitor_event(self, level, msg):
+ # events provided by the arm monitor
+ if "ARM_" + level in self.loggedEvents: self.registerEvent("ARM-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
+
+ def tor_ctl_event(self, level, msg):
+ # events provided by TorCtl
+ if "TORCTL_" + level in self.loggedEvents: self.registerEvent("TORCTL-%s" % level, msg, RUNLEVEL_EVENT_COLOR[level])
+
+ def write(self, msg):
+ """
+ Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
+ """
+
+ timestampStart = msg.find("[")
+ timestampEnd = msg.find("]")
+
+ level = msg[:timestampStart]
+ msg = msg[timestampEnd + 2:].strip()
+
+ if TOR_CTL_CLOSE_MSG in msg:
+ # TorCtl providing notice that control port is closed
+ self.controlPortClosed = True
+ #log.log(log.NOTICE, "Tor control port closed")
+
+ # Allows the Controller to notice that tor's shut down.
+ # TODO: should make the controller the torctl event listener rather than
+ # this log panel (it'll also make this less hacky)
+ torTools.getConn().isAlive()
+ self.tor_ctl_event(level, msg)
+
+ def flush(self): pass
+
+ def registerEvent(self, type, msg, color):
+ """
+ Notes event and redraws log. If paused it's held in a temporary buffer. If
+ msg is a list then this is expanded to multiple lines.
+ """
+
+ if not type.startswith("ARM"): self.lastHeartbeat = time.time()
+ eventTime = self.eventTimeOverwrite if self.eventTimeOverwrite else time.localtime()
+ toAdd = []
+
+ # wraps if a single line message
+ if isinstance(msg, str): msg = [msg]
+
+ firstLine = True
+ for msgLine in msg:
+ # strips control characters to avoid screwing up the terminal
+ msgLine = "".join([char for char in msgLine if isprint(char)])
+
+ header = "%02i:%02i:%02i %s" % (eventTime[3], eventTime[4], eventTime[5], "[%s]" % type) if firstLine else ""
+ toAdd.append("%s %s" % (header, msgLine))
+ firstLine = False
+
+ toAdd.reverse()
+ if self.isPaused:
+ for msgLine in toAdd: self.pauseBuffer.insert(0, (msgLine, color))
+ if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
+ else:
+ for msgLine in toAdd: self.msgLog.insert(0, (msgLine, color))
+ if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
+ self.redraw(True)
+
+ def draw(self, subwindow, width, height):
+ """
+ Redraws message log. Entries stretch to use available space and may
+ contain up to two lines. Starts with newest entries.
+ """
+
+ isScrollBarVisible = self.getLogDisplayLength() > height - 1
+ xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
+
+ # draws label - uses ellipsis if too long, for instance:
+ # Events (DEBUG, INFO, NOTICE, WARN...):
+ eventsLabel = "Events"
+
+ # separates tor and arm runlevels (might be able to show as range)
+ eventsList = list(self.loggedEvents)
+ torRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, ""))
+ armRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "ARM_"))
+ torctlRunlevelLabel = ", ".join(parseRunlevelRanges(eventsList, "TORCTL_"))
+
+ if torctlRunlevelLabel: eventsList = ["TORCTL " + torctlRunlevelLabel] + eventsList
+ if armRunlevelLabel: eventsList = ["ARM " + armRunlevelLabel] + eventsList
+ if torRunlevelLabel: eventsList = [torRunlevelLabel] + eventsList
+
+ eventsListing = ", ".join(eventsList)
+ filterLabel = "" if not self.regexFilter else " - filter: %s" % self.regexFilter.pattern
+
+ firstLabelLen = eventsListing.find(", ")
+ if firstLabelLen == -1: firstLabelLen = len(eventsListing)
+ else: firstLabelLen += 3
+
+ if width > 10 + firstLabelLen:
+ eventsLabel += " ("
+
+ if len(eventsListing) > width - 11:
+ labelBreak = eventsListing[:width - 12].rfind(", ")
+ eventsLabel += "%s..." % eventsListing[:labelBreak]
+ elif len(eventsListing) + len(filterLabel) > width - 11:
+ eventsLabel += eventsListing
+ else: eventsLabel += eventsListing + filterLabel
+ eventsLabel += ")"
+ eventsLabel += ":"
+
+ self.addstr(0, 0, eventsLabel, curses.A_STANDOUT)
+
+ # log entries
+ maxLoc = self.getLogDisplayLength() - height + 1
+ self.scroll = max(0, min(self.scroll, maxLoc))
+ lineCount = 1 - self.scroll
+
+ for (line, color) in self.msgLog:
+ if self.regexFilter and not self.regexFilter.search(line):
+ continue # filter doesn't match log message - skip
+
+ # splits over too lines if too long
+ if len(line) < width:
+ if lineCount >= 1: self.addstr(lineCount, xOffset, line, uiTools.getColor(color))
+ lineCount += 1
+ else:
+ (line1, line2) = splitLine(line, width - xOffset)
+ if lineCount >= 1: self.addstr(lineCount, xOffset, line1, uiTools.getColor(color))
+ if lineCount >= 0: self.addstr(lineCount + 1, xOffset, line2, uiTools.getColor(color))
+ lineCount += 2
+
+ if lineCount >= height: break # further log messages wouldn't fit
+
+ if isScrollBarVisible: self.addScrollBar(self.scroll, self.scroll + height - 1, self.getLogDisplayLength(), 1)
+
+ def getLogDisplayLength(self):
+ """
+ Provides the number of lines the log would currently occupy.
+ """
+
+ logLength = len(self.msgLog)
+
+ # takes into account filtered and wrapped messages
+ for (line, color) in self.msgLog:
+ if self.regexFilter and not self.regexFilter.search(line): logLength -= 1
+ elif len(line) >= self.getPreferredSize()[1]: logLength += 1
+
+ return logLength
+
+ 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.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
+ if self.win: self.redraw(True) # hack to avoid redrawing during init
+
+ def getHeartbeat(self):
+ """
+ Provides the number of seconds since the last registered event (this always
+ listens to BW events so should be less than a second if relay's still
+ responsive).
+ """
+
+ return time.time() - self.lastHeartbeat
+
+def parseRunlevelRanges(eventsList, searchPrefix):
+ """
+ This parses a list of events to provide an ordered list of runlevels,
+ condensed if three or more are in a contiguous range. This removes parsed
+ runlevels from the eventsList. For instance:
+
+ eventsList = ["BW", "ARM_WARN", "ERR", "ARM_ERR", "ARM_DEBUG", "ARM_NOTICE"]
+ searchPrefix = "ARM_"
+
+ results in:
+ eventsList = ["BW", "ERR"]
+ return value is ["DEBUG", "NOTICE - ERR"]
+
+ """
+
+ # blank ending runlevel forces the break condition to be reached at the end
+ runlevels = ["DEBUG", "INFO", "NOTICE", "WARN", "ERR", ""]
+ runlevelLabels = []
+ start, end = "", ""
+ rangeLength = 0
+
+ for level in runlevels:
+ if searchPrefix + level in eventsList:
+ eventsList.remove(searchPrefix + level)
+
+ if start:
+ end = level
+ rangeLength += 1
+ else:
+ start = level
+ rangeLength = 1
+ elif rangeLength > 0:
+ # reached a break in the runlevels
+ if rangeLength == 1: runlevelLabels += [start]
+ elif rangeLength == 2: runlevelLabels += [start, end]
+ else: runlevelLabels += ["%s - %s" % (start, end)]
+
+ start, end = "", ""
+ rangeLength = 0
+
+ return runlevelLabels
+
+def splitLine(message, x):
+ """
+ Divides message into two lines, attempting to do it on a wordbreak.
+ """
+
+ lastWordbreak = message[:x].rfind(" ")
+ if x - lastWordbreak < 10:
+ line1 = message[:lastWordbreak]
+ line2 = " %s" % message[lastWordbreak:].strip()
+ else:
+ # over ten characters until the last word - dividing
+ line1 = "%s-" % message[:x - 2]
+ line2 = " %s" % message[x - 2:].strip()
+
+ # ends line with ellipsis if too long
+ if len(line2) > x:
+ lastWordbreak = line2[:x - 4].rfind(" ")
+
+ # doesn't use wordbreak if it's a long word or the whole line is one
+ # word (picking up on two space indent to have index 1)
+ if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
+ line2 = "%s..." % line2[:lastWordbreak]
+
+ return (line1, line2)
+
Copied: arm/trunk/src/prereq.py (from rev 22947, arm/trunk/init/prereq.py)
===================================================================
--- arm/trunk/src/prereq.py (rev 0)
+++ arm/trunk/src/prereq.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,23 @@
+"""
+Provides a warning and error code if python version isn't compatible.
+"""
+
+import sys
+
+if __name__ == '__main__':
+ majorVersion = sys.version_info[0]
+ minorVersion = sys.version_info[1]
+
+ if majorVersion > 2:
+ print("arm isn't compatible beyond the python 2.x series\n")
+ sys.exit(1)
+ elif majorVersion < 2 or minorVersion < 5:
+ print("arm requires python version 2.5 or greater\n")
+ sys.exit(1)
+
+ try:
+ import curses
+ except ImportError:
+ print("arm requires curses - try installing the python-curses package\n")
+ sys.exit(1)
+
Copied: arm/trunk/src/starter.py (from rev 22947, arm/trunk/init/starter.py)
===================================================================
--- arm/trunk/src/starter.py (rev 0)
+++ arm/trunk/src/starter.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,177 @@
+#!/usr/bin/env python
+
+"""
+Command line application for monitoring Tor relays, providing real time status
+information. This is the starter for the application, handling and validating
+command line parameters.
+"""
+
+import os
+import sys
+import getopt
+
+import interface.controller
+import interface.logPanel
+import util.conf
+import util.connections
+import util.hostnames
+import util.log
+import util.panel
+import util.sysTools
+import util.torTools
+import util.uiTools
+import TorCtl.TorUtil
+
+VERSION = "1.3.6_dev"
+LAST_MODIFIED = "July 7, 2010"
+
+DEFAULT_CONFIG = os.path.expanduser("~/.armrc")
+DEFAULTS = {"startup.controlPassword": None,
+ "startup.interface.ipAddress": "127.0.0.1",
+ "startup.interface.port": 9051,
+ "startup.blindModeEnabled": False,
+ "startup.events": "N3"}
+
+OPT = "i:c:be:vh"
+OPT_EXPANDED = ["interface=", "config=", "blind", "event=", "version", "help"]
+HELP_MSG = """Usage arm [OPTION]
+Terminal status monitor for Tor relays.
+
+ -i, --interface [ADDRESS:]PORT change control interface from %s:%i
+ -c, --config CONFIG_PATH loaded configuration options, CONFIG_PATH
+ defaults to: %s
+ -b, --blind disable connection lookups
+ -e, --event EVENT_FLAGS event types in message log (default: %s)
+%s
+ -v, --version provides version information
+ -h, --help presents this help
+
+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
+""" % (DEFAULTS["startup.interface.ipAddress"], DEFAULTS["startup.interface.port"], DEFAULT_CONFIG, DEFAULTS["startup.events"], interface.logPanel.EVENT_LISTING)
+
+def isValidIpAddr(ipStr):
+ """
+ Returns true if input is a valid IPv4 address, false otherwise.
+ """
+
+ for i in range(4):
+ if i < 3:
+ divIndex = ipStr.find(".")
+ if divIndex == -1: return False # expected a period to be valid
+ octetStr = ipStr[:divIndex]
+ ipStr = ipStr[divIndex + 1:]
+ else:
+ octetStr = ipStr
+
+ try:
+ octet = int(octetStr)
+ if not octet >= 0 or not octet <= 255: return False
+ except ValueError:
+ # address value isn't an integer
+ return False
+
+ return True
+
+if __name__ == '__main__':
+ param = dict([(key, None) for key in DEFAULTS.keys()])
+ configPath = DEFAULT_CONFIG # path used for customized configuration
+
+ # parses user input, noting any issues
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], OPT, OPT_EXPANDED)
+ except getopt.GetoptError, exc:
+ print str(exc) + " (for usage provide --help)"
+ sys.exit()
+
+ for opt, arg in opts:
+ if opt in ("-i", "--interface"):
+ # defines control interface address/port
+ controlAddr, controlPort = None, None
+ divIndex = arg.find(":")
+
+ try:
+ if divIndex == -1:
+ controlPort = int(arg)
+ else:
+ controlAddr = arg[0:divIndex]
+ controlPort = int(arg[divIndex + 1:])
+ except ValueError:
+ print "'%s' isn't a valid port number" % arg
+ sys.exit()
+
+ param["startup.interface.ipAddress"] = controlAddr
+ param["startup.interface.port"] = controlPort
+ elif opt in ("-c", "--config"): configPath = arg # sets path of user's config
+ elif opt in ("-b", "--blind"):
+ param["startup.blindModeEnabled"] = True # prevents connection lookups
+ elif opt in ("-e", "--event"):
+ param["startup.events"] = arg # set event flags
+ elif opt in ("-v", "--version"):
+ print "arm version %s (released %s)\n" % (VERSION, LAST_MODIFIED)
+ sys.exit()
+ elif opt in ("-h", "--help"):
+ print HELP_MSG
+ sys.exit()
+
+ # attempts to load user's custom configuration
+ config = util.conf.getConfig("arm")
+ config.path = configPath
+
+ if os.path.exists(configPath):
+ try:
+ config.load()
+
+ # revises defaults to match user's configuration
+ config.update(DEFAULTS)
+
+ # loads user preferences for utilities
+ for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.sysTools, util.torTools, util.uiTools):
+ utilModule.loadConfig(config)
+ except IOError, exc:
+ msg = "Failed to load configuration (using defaults): \"%s\"" % str(exc)
+ util.log.log(util.log.WARN, msg)
+ else:
+ msg = "No configuration found at '%s', using defaults" % configPath
+ util.log.log(util.log.NOTICE, msg)
+
+ # overwrites undefined parameters with defaults
+ for key in param.keys():
+ if param[key] == None: param[key] = DEFAULTS[key]
+
+ # validates that input has a valid ip address and port
+ controlAddr = param["startup.interface.ipAddress"]
+ controlPort = param["startup.interface.port"]
+
+ if not isValidIpAddr(controlAddr):
+ print "'%s' isn't a valid IP address" % controlAddr
+ sys.exit()
+ elif controlPort < 0 or controlPort > 65535:
+ print "'%s' isn't a valid port number (ports range 0-65535)" % controlPort
+ sys.exit()
+
+ # validates and expands log event flags
+ try:
+ expandedEvents = interface.logPanel.expandEvents(param["startup.events"])
+ except ValueError, exc:
+ for flag in str(exc):
+ print "Unrecognized event flag: %s" % flag
+ sys.exit()
+
+ # temporarily disables TorCtl logging to prevent issues from going to stdout while starting
+ TorCtl.TorUtil.loglevel = "NONE"
+
+ # sets up TorCtl connection, prompting for the passphrase if necessary and
+ # sending problems to stdout if they arise
+ util.torTools.INCORRECT_PASSWORD_MSG = "Controller password found in '%s' was incorrect" % configPath
+ authPassword = config.get("startup.controlPassword", DEFAULTS["startup.controlPassword"])
+ conn = util.torTools.connect(controlAddr, controlPort, authPassword)
+ if conn == None: sys.exit(1)
+
+ controller = util.torTools.getConn()
+ controller.init(conn)
+
+ interface.controller.startTorMonitor(expandedEvents, param["startup.blindModeEnabled"])
+ conn.close()
+
Deleted: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/util/torTools.py 2010-08-18 12:14:15 UTC (rev 22947)
+++ arm/trunk/src/util/torTools.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -1,1004 +0,0 @@
-"""
-Helper for working with an active tor process. This both provides a wrapper for
-accessing TorCtl and notifications of state changes to subscribers. To quickly
-fetch a TorCtl instance to experiment with use the following:
-
->>> import util.torTools
->>> conn = util.torTools.connect()
->>> conn.get_info("version")["version"]
-'0.2.1.24'
-"""
-
-import os
-import time
-import socket
-import getpass
-import thread
-import threading
-
-from TorCtl import TorCtl, TorUtil
-
-import log
-import sysTools
-
-# enums for tor's controller state:
-# TOR_INIT - attached to a new controller or restart/sighup signal received
-# TOR_CLOSED - control port closed
-TOR_INIT, TOR_CLOSED = range(1, 3)
-
-# message logged by default when a controller can't set an event type
-DEFAULT_FAILED_EVENT_MSG = "Unsupported event type: %s"
-
-# TODO: check version when reattaching to controller and if version changes, flush?
-# Skips attempting to set events we've failed to set before. This avoids
-# logging duplicate warnings but can be problematic if controllers belonging
-# to multiple versions of tor are attached, making this unreflective of the
-# controller's capabilites. However, this is a pretty bizarre edge case.
-DROP_FAILED_EVENTS = True
-FAILED_EVENTS = set()
-
-CONTROLLER = None # singleton Controller instance
-INCORRECT_PASSWORD_MSG = "Provided passphrase was incorrect"
-
-# valid keys for the controller's getInfo cache
-CACHE_ARGS = ("nsEntry", "descEntry", "bwRate", "bwBurst", "bwObserved",
- "bwMeasured", "flags", "fingerprint", "pid")
-
-TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
-UNKNOWN = "UNKNOWN" # value used by cached information if undefined
-CONFIG = {"log.torGetInfo": log.DEBUG, "log.torGetConf": log.DEBUG}
-
-# events used for controller functionality:
-# NOTICE - used to detect when tor is shut down
-# NEWDESC, NS, and NEWCONSENSUS - used for cache invalidation
-REQ_EVENTS = {"NOTICE": "this will be unable to detect when tor is shut down",
- "NEWDESC": "information related to descriptors will grow stale",
- "NS": "information related to the consensus will grow stale",
- "NEWCONSENSUS": "information related to the consensus will grow stale"}
-
-def loadConfig(config):
- config.update(CONFIG)
-
-def makeCtlConn(controlAddr="127.0.0.1", controlPort=9051):
- """
- Opens a socket to the tor controller and queries its authentication type,
- raising an IOError if problems occur. The result of this function is a tuple
- of the TorCtl connection and the authentication type, where the later is one
- of the following:
- "NONE" - no authentication required
- "PASSWORD" - requires authentication via a hashed password
- "COOKIE=<FILE>" - requires the specified authentication cookie
-
- Arguments:
- controlAddr - ip address belonging to the controller
- controlPort - port belonging to the controller
- """
-
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.connect((controlAddr, controlPort))
- conn = TorCtl.Connection(s)
- except socket.error, exc:
- if "Connection refused" in exc.args:
- # most common case - tor control port isn't available
- raise IOError("Connection refused. Is the ControlPort enabled?")
- else: raise IOError("Failed to establish socket: %s" % exc)
-
- # check PROTOCOLINFO for authentication type
- try:
- authInfo = conn.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
- except TorCtl.ErrorReply, exc:
- raise IOError("Unable to query PROTOCOLINFO for authentication type: %s" % exc)
-
- if authInfo.startswith("AUTH METHODS=NULL"):
- # no authentication required
- return (conn, "NONE")
- elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
- # password authentication
- return (conn, "PASSWORD")
- elif authInfo.startswith("AUTH METHODS=COOKIE"):
- # cookie authentication, parses authentication cookie path
- start = authInfo.find("COOKIEFILE=\"") + 12
- end = authInfo.find("\"", start)
- return (conn, "COOKIE=%s" % authInfo[start:end])
-
-def initCtlConn(conn, authType="NONE", authVal=None):
- """
- Authenticates to a tor connection. The authentication type can be any of the
- following strings:
- NONE, PASSWORD, COOKIE
-
- if the authentication type is anything other than NONE then either a
- passphrase or path to an authentication cookie is expected. If an issue
- arises this raises either of the following:
- - IOError for failures in reading an authentication cookie
- - TorCtl.ErrorReply for authentication failures
-
- Argument:
- conn - unauthenticated TorCtl connection
- authType - type of authentication method to use
- authVal - passphrase or path to authentication cookie
- """
-
- # validates input
- if authType not in ("NONE", "PASSWORD", "COOKIE"):
- # authentication type unrecognized (possibly a new addition to the controlSpec?)
- raise TorCtl.ErrorReply("Unrecognized authentication type: %s" % authType)
- elif authType != "NONE" and authVal == None:
- typeLabel = "passphrase" if authType == "PASSWORD" else "cookie"
- raise TorCtl.ErrorReply("Unable to authenticate: no %s provided" % typeLabel)
-
- authCookie = None
- try:
- if authType == "NONE": conn.authenticate("")
- elif authType == "PASSWORD": conn.authenticate(authVal)
- else:
- authCookie = open(authVal, "r")
- conn.authenticate_cookie(authCookie)
- authCookie.close()
- except TorCtl.ErrorReply, exc:
- if authCookie: authCookie.close()
- issue = str(exc)
-
- # simplifies message if the wrong credentials were provided (common mistake)
- if issue.startswith("515 Authentication failed: "):
- if issue[27:].startswith("Password did not match"):
- issue = "password incorrect"
- elif issue[27:] == "Wrong length on authentication cookie.":
- issue = "cookie value incorrect"
-
- raise TorCtl.ErrorReply("Unable to authenticate: %s" % issue)
- except IOError, exc:
- if authCookie: authCookie.close()
- issue = None
-
- # cleaner message for common errors
- if str(exc).startswith("[Errno 13] Permission denied"): issue = "permission denied"
- elif str(exc).startswith("[Errno 2] No such file or directory"): issue = "file doesn't exist"
-
- # if problem's recognized give concise message, otherwise print exception string
- if issue: raise IOError("Failed to read authentication cookie (%s): %s" % (issue, authVal))
- else: raise IOError("Failed to read authentication cookie: %s" % exc)
-
-def connect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
- """
- Convenience method for quickly getting a TorCtl connection. This is very
- handy for debugging or CLI setup, handling setup and prompting for a password
- if necessary (if either none is provided as input or it fails). If any issues
- arise this prints a description of the problem and returns None.
-
- Arguments:
- controlAddr - ip address belonging to the controller
- controlPort - port belonging to the controller
- passphrase - authentication passphrase (if defined this is used rather
- than prompting the user)
- """
-
- try:
- conn, authType = makeCtlConn(controlAddr, controlPort)
- authValue = None
-
- if authType == "PASSWORD":
- # password authentication, promting for the password if it wasn't provided
- if passphrase: authValue = passphrase
- else:
- try: authValue = getpass.getpass()
- except KeyboardInterrupt: return None
- elif authType.startswith("COOKIE"):
- authType, authValue = authType.split("=", 1)
-
- initCtlConn(conn, authType, authValue)
- return conn
- except Exception, exc:
- if passphrase and str(exc) == "Unable to authenticate: password incorrect":
- # provide a warning that the provided password didn't work, then try
- # again prompting for the user to enter it
- print INCORRECT_PASSWORD_MSG
- return connect(controlAddr, controlPort)
- else:
- print exc
- return None
-
-def getPid(controlPort=9051, pidFilePath=None):
- """
- Attempts to determine the process id for a running tor process, using the
- following:
- 1. GETCONF PidFile
- 2. "pidof tor"
- 3. "netstat -npl | grep 127.0.0.1:%s" % <tor control port>
- 4. "ps -o pid -C tor"
-
- If pidof or ps provide multiple tor instances then their results are
- discarded (since only netstat can differentiate using the control port). This
- provides None if either no running process exists or it can't be determined.
-
- Arguments:
- controlPort - control port of the tor process if multiple exist
- pidFilePath - path to the pid file generated by tor
- """
-
- # attempts to fetch via the PidFile, failing if:
- # - the option is unset
- # - unable to read the file (such as insufficient permissions)
-
- if pidFilePath:
- try:
- pidFile = open(pidFilePath, "r")
- pidEntry = pidFile.readline().strip()
- pidFile.close()
-
- if pidEntry.isdigit(): return pidEntry
- except Exception: pass
-
- # attempts to resolve using pidof, failing if:
- # - tor's running under a different name
- # - there's multiple instances of tor
- try:
- results = sysTools.call("pidof tor")
- if len(results) == 1 and len(results[0].split()) == 1:
- pid = results[0].strip()
- if pid.isdigit(): return pid
- except IOError: pass
-
- # attempts to resolve using netstat, failing if:
- # - tor's being run as a different user due to permissions
- try:
- results = sysTools.call("netstat -npl | grep 127.0.0.1:%i" % controlPort)
-
- if len(results) == 1:
- results = results[0].split()[6] # process field (ex. "7184/tor")
- pid = results[:results.find("/")]
- if pid.isdigit(): return pid
- except IOError: pass
-
- # attempts to resolve using ps, failing if:
- # - tor's running under a different name
- # - there's multiple instances of tor
- try:
- results = sysTools.call("ps -o pid -C tor")
- if len(results) == 2:
- pid = results[1].strip()
- if pid.isdigit(): return pid
- except IOError: pass
-
- return None
-
-def getConn():
- """
- Singleton constructor for a Controller. Be aware that this start
- uninitialized, needing a TorCtl instance before it's fully functional.
- """
-
- global CONTROLLER
- if CONTROLLER == None: CONTROLLER = Controller()
- return CONTROLLER
-
-class Controller(TorCtl.PostEventListener):
- """
- TorCtl wrapper providing convenience functions, listener functionality for
- tor's state, and the capability for controller connections to be restarted
- if closed.
- """
-
- def __init__(self):
- TorCtl.PostEventListener.__init__(self)
- self.conn = None # None if uninitialized or controller's been closed
- self.connLock = threading.RLock()
- self.eventListeners = [] # instances listening for tor controller events
- self.torctlListeners = [] # callback functions for TorCtl events
- self.statusListeners = [] # callback functions for tor's state changes
- self.controllerEvents = [] # list of successfully set controller events
- self._isReset = False # internal flag for tracking resets
- self._status = TOR_CLOSED # current status of the attached control port
- self._statusTime = 0 # unix time-stamp for the duration of the status
- self.lastHeartbeat = 0 # time of the last tor event
-
- # cached getInfo parameters (None if unset or possibly changed)
- self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
-
- # directs TorCtl to notify us of events
- TorUtil.loglevel = "DEBUG"
- TorUtil.logfile = self
-
- def init(self, conn=None):
- """
- Uses the given TorCtl instance for future operations, notifying listeners
- about the change.
-
- Arguments:
- conn - TorCtl instance to be used, if None then a new instance is fetched
- via the connect function
- """
-
- if conn == None:
- conn = connect()
-
- if conn == None: raise ValueError("Unable to initialize TorCtl instance.")
-
- if conn.is_live() and conn != self.conn:
- self.connLock.acquire()
-
- if self.conn: self.close() # shut down current connection
- self.conn = conn
- self.conn.add_event_listener(self)
- for listener in self.eventListeners: self.conn.add_event_listener(listener)
-
- # sets the events listened for by the new controller (incompatible events
- # are dropped with a logged warning)
- self.setControllerEvents(self.controllerEvents)
-
- self.connLock.release()
-
- self._status = TOR_INIT
- self._statusTime = time.time()
-
- # notifies listeners that a new controller is available
- thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
-
- def close(self):
- """
- Closes the current TorCtl instance and notifies listeners.
- """
-
- self.connLock.acquire()
- if self.conn:
- self.conn.close()
- self.conn = None
- self.connLock.release()
-
- self._status = TOR_CLOSED
- self._statusTime = time.time()
-
- # notifies listeners that the controller's been shut down
- thread.start_new_thread(self._notifyStatusListeners, (TOR_CLOSED,))
- else: self.connLock.release()
-
- def isAlive(self):
- """
- Returns True if this has been initialized with a working TorCtl instance,
- False otherwise.
- """
-
- self.connLock.acquire()
-
- result = False
- if self.conn:
- if self.conn.is_live(): result = True
- else: self.close()
-
- self.connLock.release()
- return result
-
- def getHeartbeat(self):
- """
- Provides the time of the last registered tor event (if listening for BW
- events then this should occure every second if relay's still responsive).
- This returns zero if this has never received an event.
- """
-
- return self.lastHeartbeat
-
- def getTorCtl(self):
- """
- Provides the current TorCtl connection. If unset or closed then this
- returns None.
- """
-
- self.connLock.acquire()
- result = None
- if self.isAlive(): result = self.conn
- self.connLock.release()
-
- return result
-
- def getInfo(self, param, default = None, suppressExc = True):
- """
- Queries the control port for the given GETINFO option, providing the
- default if the response is undefined or fails for any reason (error
- response, control port closed, initiated, etc).
-
- Arguments:
- param - GETINFO option to be queried
- default - result if the query fails and exception's suppressed
- suppressExc - suppresses lookup errors (returning the default) if true,
- otherwise this raises the original exception
- """
-
- self.connLock.acquire()
-
- startTime = time.time()
- result, raisedExc = default, None
- if self.isAlive():
- try:
- getInfoVal = self.conn.get_info(param)[param]
- if getInfoVal != None: result = getInfoVal
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
- if type(exc) == TorCtl.TorCtlClosed: self.close()
- raisedExc = exc
-
- msg = "tor control call: GETINFO %s (runtime: %0.4f)" % (param, time.time() - startTime)
- log.log(CONFIG["log.torGetInfo"], msg)
-
- self.connLock.release()
-
- if not suppressExc and raisedExc: raise raisedExc
- else: return result
-
- def getOption(self, param, default = None, multiple = False, suppressExc = True):
- """
- Queries the control port for the given configuration option, providing the
- default if the response is undefined or fails for any reason. If multiple
- values exist then this arbitrarily returns the first unless the multiple
- flag is set.
-
- Arguments:
- param - configuration option to be queried
- default - result if the query fails and exception's suppressed
- multiple - provides a list of results if true, otherwise this just
- returns the first value
- suppressExc - suppresses lookup errors (returning the default) if true,
- otherwise this raises the original exception
- """
-
- self.connLock.acquire()
-
- startTime = time.time()
- result, raisedExc = [], None
- if self.isAlive():
- try:
- if multiple:
- for key, value in self.conn.get_option(param):
- if value != None: result.append(value)
- else:
- getConfVal = self.conn.get_option(param)[0][1]
- if getConfVal != None: result = getConfVal
- except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
- if type(exc) == TorCtl.TorCtlClosed: self.close()
- result, raisedExc = default, exc
-
- msg = "tor control call: GETCONF %s (runtime: %0.4f)" % (param, time.time() - startTime)
- log.log(CONFIG["log.torGetConf"], msg)
-
- self.connLock.release()
-
- if not suppressExc and raisedExc: raise raisedExc
- elif result == []: return default
- else: return result
-
- def getMyNetworkStatus(self, default = None):
- """
- Provides the network status entry for this relay if available. This is
- occasionally expanded so results may vary depending on tor's version. For
- 0.2.2.13 they contained entries like the following:
-
- r caerSidi p1aag7VwarGxqctS7/fS0y5FU+s 9On1TRGCEpljszPpJR1hKqlzaY8 2010-05-26 09:26:06 76.104.132.98 9001 0
- s Fast HSDir Named Running Stable Valid
- w Bandwidth=25300
- p reject 1-65535
-
- Arguments:
- default - result if the query fails
- """
-
- return self._getRelayAttr("nsEntry", default)
-
- def getMyDescriptor(self, default = None):
- """
- Provides the descriptor entry for this relay if available.
-
- Arguments:
- default - result if the query fails
- """
-
- return self._getRelayAttr("descEntry", default)
-
- def getMyBandwidthRate(self, default = None):
- """
- Provides the effective relaying bandwidth rate of this relay. Currently
- this doesn't account for SETCONF events.
-
- Arguments:
- default - result if the query fails
- """
-
- return self._getRelayAttr("bwRate", default)
-
- def getMyBandwidthBurst(self, default = None):
- """
- Provides the effective bandwidth burst rate of this relay. Currently this
- doesn't account for SETCONF events.
-
- Arguments:
- default - result if the query fails
- """
-
- return self._getRelayAttr("bwBurst", default)
-
- def getMyBandwidthObserved(self, default = None):
- """
- Provides the relay's current observed bandwidth (the throughput determined
- from historical measurements on the client side). This is used in the
- heuristic used for path selection if the measured bandwidth is undefined.
- This is fetched from the descriptors and hence will get stale if
- descriptors aren't periodically updated.
-
- Arguments:
- default - result if the query fails
- """
-
- return self._getRelayAttr("bwObserved", default)
-
- def getMyBandwidthMeasured(self, default = None):
- """
- Provides the relay's current measured bandwidth (the throughput as noted by
- the directory authorities and used by clients for relay selection). This is
- undefined if not in the consensus or with older versions of Tor. Depending
- on the circumstances this can be from a variety of things (observed,
- measured, weighted measured, etc) as described by:
- https://trac.torproject.org/projects/tor/ticket/1566
-
- Arguments:
- default - result if the query fails
- """
-
- return self._getRelayAttr("bwMeasured", default)
-
- def getMyFingerprint(self, default = None):
- """
- Provides the fingerprint for this relay.
-
- Arguments:
- default - result if the query fails
- """
-
- return self._getRelayAttr("fingerprint", default, False)
-
- def getMyFlags(self, default = None):
- """
- Provides the flags held by this relay.
-
- Arguments:
- default - result if the query fails or this relay isn't a part of the consensus yet
- """
-
- return self._getRelayAttr("flags", default)
-
- def getMyPid(self):
- """
- Provides the pid of the attached tor process (None if no controller exists
- or this can't be determined).
- """
-
- return self._getRelayAttr("pid", None)
-
- def getStatus(self):
- """
- Provides a tuple consisting of the control port's current status and unix
- time-stamp for when it became this way (zero if no status has yet to be
- set).
- """
-
- return (self._status, self._statusTime)
-
- def addEventListener(self, listener):
- """
- Directs further tor controller events to callback functions of the
- listener. If a new control connection is initialized then this listener is
- reattached.
-
- Arguments:
- listener - TorCtl.PostEventListener instance listening for events
- """
-
- self.connLock.acquire()
- self.eventListeners.append(listener)
- if self.isAlive(): self.conn.add_event_listener(listener)
- self.connLock.release()
-
- def addTorCtlListener(self, callback):
- """
- Directs further TorCtl events to the callback function. Events are composed
- of a runlevel and message tuple.
-
- Arguments:
- callback - functor that'll accept the events, expected to be of the form:
- myFunction(runlevel, msg)
- """
-
- self.torctlListeners.append(callback)
-
- def addStatusListener(self, callback):
- """
- Directs further events related to tor's controller status to the callback
- function.
-
- Arguments:
- callback - functor that'll accept the events, expected to be of the form:
- myFunction(controller, eventType)
- """
-
- self.statusListeners.append(callback)
-
- def removeStatusListener(self, callback):
- """
- Stops listener from being notified of further events. This returns true if a
- listener's removed, false otherwise.
-
- Arguments:
- callback - functor to be removed
- """
-
- if callback in self.statusListeners:
- self.statusListeners.remove(callback)
- return True
- else: return False
-
- def getControllerEvents(self):
- """
- Provides the events the controller's currently configured to listen for.
- """
-
- return list(self.controllerEvents)
-
- def setControllerEvents(self, events):
- """
- Sets the events being requested from any attached tor instance, logging
- warnings for event types that aren't supported (possibly due to version
- issues). Events in REQ_EVENTS will also be included, logging at the error
- level with an additional description in case of failure.
-
- This remembers the successfully set events and tries to request them from
- any tor instance it attaches to in the future too (again logging and
- dropping unsuccessful event types).
-
- This returns the listing of event types that were successfully set. If not
- currently attached to a tor instance then all events are assumed to be ok,
- then attempted when next attached to a control port.
-
- Arguments:
- events - listing of events to be set
- """
-
- self.connLock.acquire()
-
- returnVal = []
- if self.isAlive():
- events = set(events)
- events = events.union(set(REQ_EVENTS.keys()))
- unavailableEvents = set()
-
- # removes anything we've already failed to set
- if DROP_FAILED_EVENTS:
- unavailableEvents.update(events.intersection(FAILED_EVENTS))
- events.difference_update(FAILED_EVENTS)
-
- # initial check for event availability, using the 'events/names' GETINFO
- # option to detect invalid events
- validEvents = self.getInfo("events/names")
-
- if validEvents:
- validEvents = set(validEvents.split())
- unavailableEvents.update(events.difference(validEvents))
- events.intersection_update(validEvents)
-
- # attempt to set events via trial and error
- isEventsSet, isAbandoned = False, False
-
- while not isEventsSet and not isAbandoned:
- try:
- self.conn.set_events(list(events))
- isEventsSet = True
- except TorCtl.ErrorReply, exc:
- msg = str(exc)
-
- if "Unrecognized event" in msg:
- # figure out type of event we failed to listen for
- start = msg.find("event \"") + 7
- end = msg.rfind("\"")
- failedType = msg[start:end]
-
- unavailableEvents.add(failedType)
- events.discard(failedType)
- else:
- # unexpected error, abandon attempt
- isAbandoned = True
- except TorCtl.TorCtlClosed:
- self.close()
- isAbandoned = True
-
- FAILED_EVENTS.update(unavailableEvents)
- if not isAbandoned:
- # logs warnings or errors for failed events
- for eventType in unavailableEvents:
- defaultMsg = DEFAULT_FAILED_EVENT_MSG % eventType
- if eventType in REQ_EVENTS:
- log.log(log.ERR, defaultMsg + " (%s)" % REQ_EVENTS[eventType])
- else:
- log.log(log.WARN, defaultMsg)
-
- self.controllerEvents = list(events)
- returnVal = list(events)
- else:
- # attempts to set the events when next attached to a control port
- self.controllerEvents = list(events)
- returnVal = list(events)
-
- self.connLock.release()
- return returnVal
-
- def reload(self, issueSighup = False):
- """
- This resets tor (sending a RELOAD signal to the control port) causing tor's
- internal state to be reset and the torrc reloaded. This can either be done
- by...
- - the controller via a RELOAD signal (default and suggested)
- conn.send_signal("RELOAD")
- - system reload signal (hup)
- pkill -sighup tor
-
- The later isn't really useful unless there's some reason the RELOAD signal
- won't do the trick. Both methods raise an IOError in case of failure.
-
- Arguments:
- issueSighup - issues a sighup rather than a controller RELOAD signal
- """
-
- self.connLock.acquire()
-
- raisedException = None
- if self.isAlive():
- if not issueSighup:
- try:
- self.conn.send_signal("RELOAD")
- except Exception, exc:
- # new torrc parameters caused an error (tor's likely shut down)
- # BUG: this doesn't work - torrc errors still cause TorCtl to crash... :(
- # http://bugs.noreply.org/flyspray/index.php?do=details&id=1329
- raisedException = IOError(str(exc))
- else:
- try:
- # Redirects stderr to stdout so we can check error status (output
- # should be empty if successful). Example error:
- # pkill: 5592 - Operation not permitted
- #
- # note that this may provide multiple errors, even if successful,
- # hence this:
- # - only provide an error if Tor fails to log a sighup
- # - provide the error message associated with the tor pid (others
- # would be a red herring)
- if not sysTools.isAvailable("pkill"):
- raise IOError("pkill command is unavailable")
-
- self._isReset = False
- pkillCall = os.popen("pkill -sighup ^tor$ 2> /dev/stdout")
- pkillOutput = pkillCall.readlines()
- pkillCall.close()
-
- # Give the sighupTracker a moment to detect the sighup signal. This
- # is, of course, a possible concurrency bug. However I'm not sure
- # of a better method for blocking on this...
- waitStart = time.time()
- while time.time() - waitStart < 1:
- time.sleep(0.1)
- if self._isReset: break
-
- if not self._isReset:
- errorLine, torPid = "", self.getMyPid()
- if torPid:
- for line in pkillOutput:
- if line.startswith("pkill: %s - " % torPid):
- errorLine = line
- break
-
- if errorLine: raise IOError(" ".join(errorLine.split()[3:]))
- else: raise IOError("failed silently")
- except IOError, exc:
- raisedException = exc
-
- self.connLock.release()
-
- if raisedException: raise raisedException
-
- def msg_event(self, event):
- """
- Listens for reload signal (hup), which is either produced by:
- causing the torrc and internal state to be reset.
- """
-
- if event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)"):
- self._isReset = True
-
- self._status = TOR_INIT
- self._statusTime = time.time()
-
- thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
-
- def ns_event(self, event):
- self._updateHeartbeat()
-
- myFingerprint = self.getMyFingerprint()
- if myFingerprint:
- for ns in event.nslist:
- if ns.idhex == myFingerprint:
- self._cachedParam["nsEntry"] = None
- self._cachedParam["flags"] = None
- self._cachedParam["bwMeasured"] = None
- return
- else:
- self._cachedParam["nsEntry"] = None
- self._cachedParam["flags"] = None
- self._cachedParam["bwMeasured"] = None
-
- def new_consensus_event(self, event):
- self._updateHeartbeat()
-
- self._cachedParam["nsEntry"] = None
- self._cachedParam["flags"] = None
- self._cachedParam["bwMeasured"] = None
-
- def new_desc_event(self, event):
- self._updateHeartbeat()
-
- myFingerprint = self.getMyFingerprint()
- if not myFingerprint or myFingerprint in event.idlist:
- self._cachedParam["descEntry"] = None
- self._cachedParam["bwObserved"] = None
-
- def circ_status_event(self, event):
- self._updateHeartbeat()
-
- def buildtimeout_set_event(self, event):
- self._updateHeartbeat()
-
- def stream_status_event(self, event):
- self._updateHeartbeat()
-
- def or_conn_status_event(self, event):
- self._updateHeartbeat()
-
- def stream_bw_event(self, event):
- self._updateHeartbeat()
-
- def bandwidth_event(self, event):
- self._updateHeartbeat()
-
- def address_mapped_event(self, event):
- self._updateHeartbeat()
-
- def unknown_event(self, event):
- self._updateHeartbeat()
-
- def write(self, msg):
- """
- Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
- """
-
- timestampStart, timestampEnd = msg.find("["), msg.find("]")
- level = msg[:timestampStart]
- msg = msg[timestampEnd + 2:].strip()
-
- # notifies listeners of TorCtl events
- for callback in self.torctlListeners: callback(level, msg)
-
- # checks if TorCtl is providing a notice that control port is closed
- if TOR_CTL_CLOSE_MSG in msg: self.close()
-
- def flush(self): pass
-
- def _updateHeartbeat(self):
- """
- Called on any event occurance to note the time it occured.
- """
-
- self.lastHeartbeat = time.time()
-
- def _getRelayAttr(self, key, default, cacheUndefined = True):
- """
- Provides information associated with this relay, using the cached value if
- available and otherwise looking it up.
-
- Arguments:
- key - parameter being queried (from CACHE_ARGS)
- default - value to be returned if undefined
- cacheUndefined - caches when values are undefined, avoiding further
- lookups if true
- """
-
- currentVal = self._cachedParam[key]
- if currentVal:
- if currentVal == UNKNOWN: return default
- else: return currentVal
-
- self.connLock.acquire()
-
- currentVal, result = self._cachedParam[key], None
- if not currentVal and self.isAlive():
- # still unset - fetch value
- if key in ("nsEntry", "descEntry"):
- myFingerprint = self.getMyFingerprint()
-
- if myFingerprint:
- queryType = "ns" if key == "nsEntry" else "desc"
- queryResult = self.getInfo("%s/id/%s" % (queryType, myFingerprint))
- if queryResult: result = queryResult.split("\n")
- elif key == "bwRate":
- # effective relayed bandwidth is the minimum of BandwidthRate,
- # MaxAdvertisedBandwidth, and RelayBandwidthRate (if set)
- effectiveRate = int(self.getOption("BandwidthRate"))
-
- relayRate = self.getOption("RelayBandwidthRate")
- if relayRate and relayRate != "0":
- effectiveRate = min(effectiveRate, int(relayRate))
-
- maxAdvertised = self.getOption("MaxAdvertisedBandwidth")
- if maxAdvertised: effectiveRate = min(effectiveRate, int(maxAdvertised))
-
- result = effectiveRate
- elif key == "bwBurst":
- # effective burst (same for BandwidthBurst and RelayBandwidthBurst)
- effectiveBurst = int(self.getOption("BandwidthBurst"))
-
- relayBurst = self.getOption("RelayBandwidthBurst")
- if relayBurst and relayBurst != "0":
- effectiveBurst = min(effectiveBurst, int(relayBurst))
-
- result = effectiveBurst
- elif key == "bwObserved":
- for line in self.getMyDescriptor([]):
- if line.startswith("bandwidth"):
- # line should look something like:
- # bandwidth 40960 102400 47284
- comp = line.split()
-
- if len(comp) == 4 and comp[-1].isdigit():
- result = int(comp[-1])
- break
- elif key == "bwMeasured":
- # TODO: Currently there's no client side indication of what type of
- # measurement was used. Include this in results if it's ever available.
-
- for line in self.getMyNetworkStatus([]):
- if line.startswith("w Bandwidth="):
- bwValue = line[12:]
- if bwValue.isdigit(): result = int(bwValue)
- break
- elif key == "fingerprint":
- # Fingerprints are kept until sighup if set (most likely not even a
- # setconf can change it since it's in the data directory). If orport is
- # unset then no fingerprint will be set.
- orPort = self.getOption("ORPort", "0")
- if orPort == "0": result = UNKNOWN
- else: result = self.getInfo("fingerprint")
- elif key == "flags":
- for line in self.getMyNetworkStatus([]):
- if line.startswith("s "):
- result = line[2:].split()
- break
- elif key == "pid":
- result = getPid(int(self.getOption("ControlPort", 9051)), self.getOption("PidFile"))
-
- # cache value
- if result: self._cachedParam[key] = result
- elif cacheUndefined: self._cachedParam[key] = UNKNOWN
- elif currentVal == UNKNOWN: result = currentVal
-
- self.connLock.release()
-
- if result: return result
- else: return default
-
- def _notifyStatusListeners(self, eventType):
- """
- Sends a notice to all current listeners that a given change in tor's
- controller status has occurred.
-
- Arguments:
- eventType - enum representing tor's new status
- """
-
- # resets cached getInfo parameters
- self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
-
- for callback in self.statusListeners:
- callback(self, eventType)
-
Copied: arm/trunk/src/util/torTools.py (from rev 22948, arm/trunk/util/torTools.py)
===================================================================
--- arm/trunk/src/util/torTools.py (rev 0)
+++ arm/trunk/src/util/torTools.py 2010-08-21 20:38:47 UTC (rev 23010)
@@ -0,0 +1,1009 @@
+"""
+Helper for working with an active tor process. This both provides a wrapper for
+accessing TorCtl and notifications of state changes to subscribers. To quickly
+fetch a TorCtl instance to experiment with use the following:
+
+>>> import util.torTools
+>>> conn = util.torTools.connect()
+>>> conn.get_info("version")["version"]
+'0.2.1.24'
+"""
+
+import os
+import time
+import socket
+import getpass
+import thread
+import threading
+
+from TorCtl import TorCtl, TorUtil
+
+import log
+import sysTools
+
+# enums for tor's controller state:
+# TOR_INIT - attached to a new controller or restart/sighup signal received
+# TOR_CLOSED - control port closed
+TOR_INIT, TOR_CLOSED = range(1, 3)
+
+# message logged by default when a controller can't set an event type
+DEFAULT_FAILED_EVENT_MSG = "Unsupported event type: %s"
+
+# TODO: check version when reattaching to controller and if version changes, flush?
+# Skips attempting to set events we've failed to set before. This avoids
+# logging duplicate warnings but can be problematic if controllers belonging
+# to multiple versions of tor are attached, making this unreflective of the
+# controller's capabilites. However, this is a pretty bizarre edge case.
+DROP_FAILED_EVENTS = True
+FAILED_EVENTS = set()
+
+CONTROLLER = None # singleton Controller instance
+INCORRECT_PASSWORD_MSG = "Provided passphrase was incorrect"
+
+# valid keys for the controller's getInfo cache
+CACHE_ARGS = ("nsEntry", "descEntry", "bwRate", "bwBurst", "bwObserved",
+ "bwMeasured", "flags", "fingerprint", "pid")
+
+TOR_CTL_CLOSE_MSG = "Tor closed control connection. Exiting event thread."
+UNKNOWN = "UNKNOWN" # value used by cached information if undefined
+CONFIG = {"log.torCtlPortClosed": log.NOTICE, "log.torGetInfo": log.DEBUG, "log.torGetConf": log.DEBUG}
+
+# events used for controller functionality:
+# NOTICE - used to detect when tor is shut down
+# NEWDESC, NS, and NEWCONSENSUS - used for cache invalidation
+REQ_EVENTS = {"NOTICE": "this will be unable to detect when tor is shut down",
+ "NEWDESC": "information related to descriptors will grow stale",
+ "NS": "information related to the consensus will grow stale",
+ "NEWCONSENSUS": "information related to the consensus will grow stale"}
+
+def loadConfig(config):
+ config.update(CONFIG)
+
+def makeCtlConn(controlAddr="127.0.0.1", controlPort=9051):
+ """
+ Opens a socket to the tor controller and queries its authentication type,
+ raising an IOError if problems occur. The result of this function is a tuple
+ of the TorCtl connection and the authentication type, where the later is one
+ of the following:
+ "NONE" - no authentication required
+ "PASSWORD" - requires authentication via a hashed password
+ "COOKIE=<FILE>" - requires the specified authentication cookie
+
+ Arguments:
+ controlAddr - ip address belonging to the controller
+ controlPort - port belonging to the controller
+ """
+
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect((controlAddr, controlPort))
+ conn = TorCtl.Connection(s)
+ except socket.error, exc:
+ if "Connection refused" in exc.args:
+ # most common case - tor control port isn't available
+ raise IOError("Connection refused. Is the ControlPort enabled?")
+ else: raise IOError("Failed to establish socket: %s" % exc)
+
+ # check PROTOCOLINFO for authentication type
+ try:
+ authInfo = conn.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
+ except TorCtl.ErrorReply, exc:
+ raise IOError("Unable to query PROTOCOLINFO for authentication type: %s" % exc)
+
+ if authInfo.startswith("AUTH METHODS=NULL"):
+ # no authentication required
+ return (conn, "NONE")
+ elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
+ # password authentication
+ return (conn, "PASSWORD")
+ elif authInfo.startswith("AUTH METHODS=COOKIE"):
+ # cookie authentication, parses authentication cookie path
+ start = authInfo.find("COOKIEFILE=\"") + 12
+ end = authInfo.find("\"", start)
+ return (conn, "COOKIE=%s" % authInfo[start:end])
+
+def initCtlConn(conn, authType="NONE", authVal=None):
+ """
+ Authenticates to a tor connection. The authentication type can be any of the
+ following strings:
+ NONE, PASSWORD, COOKIE
+
+ if the authentication type is anything other than NONE then either a
+ passphrase or path to an authentication cookie is expected. If an issue
+ arises this raises either of the following:
+ - IOError for failures in reading an authentication cookie
+ - TorCtl.ErrorReply for authentication failures
+
+ Argument:
+ conn - unauthenticated TorCtl connection
+ authType - type of authentication method to use
+ authVal - passphrase or path to authentication cookie
+ """
+
+ # validates input
+ if authType not in ("NONE", "PASSWORD", "COOKIE"):
+ # authentication type unrecognized (possibly a new addition to the controlSpec?)
+ raise TorCtl.ErrorReply("Unrecognized authentication type: %s" % authType)
+ elif authType != "NONE" and authVal == None:
+ typeLabel = "passphrase" if authType == "PASSWORD" else "cookie"
+ raise TorCtl.ErrorReply("Unable to authenticate: no %s provided" % typeLabel)
+
+ authCookie = None
+ try:
+ if authType == "NONE": conn.authenticate("")
+ elif authType == "PASSWORD": conn.authenticate(authVal)
+ else:
+ authCookie = open(authVal, "r")
+ conn.authenticate_cookie(authCookie)
+ authCookie.close()
+ except TorCtl.ErrorReply, exc:
+ if authCookie: authCookie.close()
+ issue = str(exc)
+
+ # simplifies message if the wrong credentials were provided (common mistake)
+ if issue.startswith("515 Authentication failed: "):
+ if issue[27:].startswith("Password did not match"):
+ issue = "password incorrect"
+ elif issue[27:] == "Wrong length on authentication cookie.":
+ issue = "cookie value incorrect"
+
+ raise TorCtl.ErrorReply("Unable to authenticate: %s" % issue)
+ except IOError, exc:
+ if authCookie: authCookie.close()
+ issue = None
+
+ # cleaner message for common errors
+ if str(exc).startswith("[Errno 13] Permission denied"): issue = "permission denied"
+ elif str(exc).startswith("[Errno 2] No such file or directory"): issue = "file doesn't exist"
+
+ # if problem's recognized give concise message, otherwise print exception string
+ if issue: raise IOError("Failed to read authentication cookie (%s): %s" % (issue, authVal))
+ else: raise IOError("Failed to read authentication cookie: %s" % exc)
+
+def connect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
+ """
+ Convenience method for quickly getting a TorCtl connection. This is very
+ handy for debugging or CLI setup, handling setup and prompting for a password
+ if necessary (if either none is provided as input or it fails). If any issues
+ arise this prints a description of the problem and returns None.
+
+ Arguments:
+ controlAddr - ip address belonging to the controller
+ controlPort - port belonging to the controller
+ passphrase - authentication passphrase (if defined this is used rather
+ than prompting the user)
+ """
+
+ try:
+ conn, authType = makeCtlConn(controlAddr, controlPort)
+ authValue = None
+
+ if authType == "PASSWORD":
+ # password authentication, promting for the password if it wasn't provided
+ if passphrase: authValue = passphrase
+ else:
+ try: authValue = getpass.getpass()
+ except KeyboardInterrupt: return None
+ elif authType.startswith("COOKIE"):
+ authType, authValue = authType.split("=", 1)
+
+ initCtlConn(conn, authType, authValue)
+ return conn
+ except Exception, exc:
+ if passphrase and str(exc) == "Unable to authenticate: password incorrect":
+ # provide a warning that the provided password didn't work, then try
+ # again prompting for the user to enter it
+ print INCORRECT_PASSWORD_MSG
+ return connect(controlAddr, controlPort)
+ else:
+ print exc
+ return None
+
+def getPid(controlPort=9051, pidFilePath=None):
+ """
+ Attempts to determine the process id for a running tor process, using the
+ following:
+ 1. GETCONF PidFile
+ 2. "pidof tor"
+ 3. "netstat -npl | grep 127.0.0.1:%s" % <tor control port>
+ 4. "ps -o pid -C tor"
+
+ If pidof or ps provide multiple tor instances then their results are
+ discarded (since only netstat can differentiate using the control port). This
+ provides None if either no running process exists or it can't be determined.
+
+ Arguments:
+ controlPort - control port of the tor process if multiple exist
+ pidFilePath - path to the pid file generated by tor
+ """
+
+ # attempts to fetch via the PidFile, failing if:
+ # - the option is unset
+ # - unable to read the file (such as insufficient permissions)
+
+ if pidFilePath:
+ try:
+ pidFile = open(pidFilePath, "r")
+ pidEntry = pidFile.readline().strip()
+ pidFile.close()
+
+ if pidEntry.isdigit(): return pidEntry
+ except Exception: pass
+
+ # attempts to resolve using pidof, failing if:
+ # - tor's running under a different name
+ # - there's multiple instances of tor
+ try:
+ results = sysTools.call("pidof tor")
+ if len(results) == 1 and len(results[0].split()) == 1:
+ pid = results[0].strip()
+ if pid.isdigit(): return pid
+ except IOError: pass
+
+ # attempts to resolve using netstat, failing if:
+ # - tor's being run as a different user due to permissions
+ try:
+ results = sysTools.call("netstat -npl | grep 127.0.0.1:%i" % controlPort)
+
+ if len(results) == 1:
+ results = results[0].split()[6] # process field (ex. "7184/tor")
+ pid = results[:results.find("/")]
+ if pid.isdigit(): return pid
+ except IOError: pass
+
+ # attempts to resolve using ps, failing if:
+ # - tor's running under a different name
+ # - there's multiple instances of tor
+ try:
+ results = sysTools.call("ps -o pid -C tor")
+ if len(results) == 2:
+ pid = results[1].strip()
+ if pid.isdigit(): return pid
+ except IOError: pass
+
+ return None
+
+def getConn():
+ """
+ Singleton constructor for a Controller. Be aware that this start
+ uninitialized, needing a TorCtl instance before it's fully functional.
+ """
+
+ global CONTROLLER
+ if CONTROLLER == None: CONTROLLER = Controller()
+ return CONTROLLER
+
+class Controller(TorCtl.PostEventListener):
+ """
+ TorCtl wrapper providing convenience functions, listener functionality for
+ tor's state, and the capability for controller connections to be restarted
+ if closed.
+ """
+
+ def __init__(self):
+ TorCtl.PostEventListener.__init__(self)
+ self.conn = None # None if uninitialized or controller's been closed
+ self.connLock = threading.RLock()
+ self.eventListeners = [] # instances listening for tor controller events
+ self.torctlListeners = [] # callback functions for TorCtl events
+ self.statusListeners = [] # callback functions for tor's state changes
+ self.controllerEvents = [] # list of successfully set controller events
+ self._isReset = False # internal flag for tracking resets
+ self._status = TOR_CLOSED # current status of the attached control port
+ self._statusTime = 0 # unix time-stamp for the duration of the status
+ self.lastHeartbeat = 0 # time of the last tor event
+
+ # cached getInfo parameters (None if unset or possibly changed)
+ self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+
+ # directs TorCtl to notify us of events
+ TorUtil.loglevel = "DEBUG"
+ TorUtil.logfile = self
+
+ def init(self, conn=None):
+ """
+ Uses the given TorCtl instance for future operations, notifying listeners
+ about the change.
+
+ Arguments:
+ conn - TorCtl instance to be used, if None then a new instance is fetched
+ via the connect function
+ """
+
+ if conn == None:
+ conn = connect()
+
+ if conn == None: raise ValueError("Unable to initialize TorCtl instance.")
+
+ if conn.is_live() and conn != self.conn:
+ self.connLock.acquire()
+
+ if self.conn: self.close() # shut down current connection
+ self.conn = conn
+ self.conn.add_event_listener(self)
+ for listener in self.eventListeners: self.conn.add_event_listener(listener)
+
+ # sets the events listened for by the new controller (incompatible events
+ # are dropped with a logged warning)
+ self.setControllerEvents(self.controllerEvents)
+
+ self.connLock.release()
+
+ self._status = TOR_INIT
+ self._statusTime = time.time()
+
+ # notifies listeners that a new controller is available
+ thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+
+ def close(self):
+ """
+ Closes the current TorCtl instance and notifies listeners.
+ """
+
+ self.connLock.acquire()
+ if self.conn:
+ self.conn.close()
+ self.conn = None
+ self.connLock.release()
+
+ self._status = TOR_CLOSED
+ self._statusTime = time.time()
+
+ # notifies listeners that the controller's been shut down
+ thread.start_new_thread(self._notifyStatusListeners, (TOR_CLOSED,))
+ else: self.connLock.release()
+
+ def isAlive(self):
+ """
+ Returns True if this has been initialized with a working TorCtl instance,
+ False otherwise.
+ """
+
+ self.connLock.acquire()
+
+ result = False
+ if self.conn:
+ if self.conn.is_live(): result = True
+ else: self.close()
+
+ self.connLock.release()
+ return result
+
+ def getHeartbeat(self):
+ """
+ Provides the time of the last registered tor event (if listening for BW
+ events then this should occure every second if relay's still responsive).
+ This returns zero if this has never received an event.
+ """
+
+ return self.lastHeartbeat
+
+ def getTorCtl(self):
+ """
+ Provides the current TorCtl connection. If unset or closed then this
+ returns None.
+ """
+
+ self.connLock.acquire()
+ result = None
+ if self.isAlive(): result = self.conn
+ self.connLock.release()
+
+ return result
+
+ def getInfo(self, param, default = None, suppressExc = True):
+ """
+ Queries the control port for the given GETINFO option, providing the
+ default if the response is undefined or fails for any reason (error
+ response, control port closed, initiated, etc).
+
+ Arguments:
+ param - GETINFO option to be queried
+ default - result if the query fails and exception's suppressed
+ suppressExc - suppresses lookup errors (returning the default) if true,
+ otherwise this raises the original exception
+ """
+
+ self.connLock.acquire()
+
+ startTime = time.time()
+ result, raisedExc = default, None
+ if self.isAlive():
+ try:
+ getInfoVal = self.conn.get_info(param)[param]
+ if getInfoVal != None: result = getInfoVal
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
+ if type(exc) == TorCtl.TorCtlClosed: self.close()
+ raisedExc = exc
+
+ msg = "tor control call: GETINFO %s (runtime: %0.4f)" % (param, time.time() - startTime)
+ log.log(CONFIG["log.torGetInfo"], msg)
+
+ self.connLock.release()
+
+ if not suppressExc and raisedExc: raise raisedExc
+ else: return result
+
+ def getOption(self, param, default = None, multiple = False, suppressExc = True):
+ """
+ Queries the control port for the given configuration option, providing the
+ default if the response is undefined or fails for any reason. If multiple
+ values exist then this arbitrarily returns the first unless the multiple
+ flag is set.
+
+ Arguments:
+ param - configuration option to be queried
+ default - result if the query fails and exception's suppressed
+ multiple - provides a list of results if true, otherwise this just
+ returns the first value
+ suppressExc - suppresses lookup errors (returning the default) if true,
+ otherwise this raises the original exception
+ """
+
+ self.connLock.acquire()
+
+ startTime = time.time()
+ result, raisedExc = [], None
+ if self.isAlive():
+ try:
+ if multiple:
+ for key, value in self.conn.get_option(param):
+ if value != None: result.append(value)
+ else:
+ getConfVal = self.conn.get_option(param)[0][1]
+ if getConfVal != None: result = getConfVal
+ except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed), exc:
+ if type(exc) == TorCtl.TorCtlClosed: self.close()
+ result, raisedExc = default, exc
+
+ msg = "tor control call: GETCONF %s (runtime: %0.4f)" % (param, time.time() - startTime)
+ log.log(CONFIG["log.torGetConf"], msg)
+
+ self.connLock.release()
+
+ if not suppressExc and raisedExc: raise raisedExc
+ elif result == []: return default
+ else: return result
+
+ def getMyNetworkStatus(self, default = None):
+ """
+ Provides the network status entry for this relay if available. This is
+ occasionally expanded so results may vary depending on tor's version. For
+ 0.2.2.13 they contained entries like the following:
+
+ r caerSidi p1aag7VwarGxqctS7/fS0y5FU+s 9On1TRGCEpljszPpJR1hKqlzaY8 2010-05-26 09:26:06 76.104.132.98 9001 0
+ s Fast HSDir Named Running Stable Valid
+ w Bandwidth=25300
+ p reject 1-65535
+
+ Arguments:
+ default - result if the query fails
+ """
+
+ return self._getRelayAttr("nsEntry", default)
+
+ def getMyDescriptor(self, default = None):
+ """
+ Provides the descriptor entry for this relay if available.
+
+ Arguments:
+ default - result if the query fails
+ """
+
+ return self._getRelayAttr("descEntry", default)
+
+ def getMyBandwidthRate(self, default = None):
+ """
+ Provides the effective relaying bandwidth rate of this relay. Currently
+ this doesn't account for SETCONF events.
+
+ Arguments:
+ default - result if the query fails
+ """
+
+ return self._getRelayAttr("bwRate", default)
+
+ def getMyBandwidthBurst(self, default = None):
+ """
+ Provides the effective bandwidth burst rate of this relay. Currently this
+ doesn't account for SETCONF events.
+
+ Arguments:
+ default - result if the query fails
+ """
+
+ return self._getRelayAttr("bwBurst", default)
+
+ def getMyBandwidthObserved(self, default = None):
+ """
+ Provides the relay's current observed bandwidth (the throughput determined
+ from historical measurements on the client side). This is used in the
+ heuristic used for path selection if the measured bandwidth is undefined.
+ This is fetched from the descriptors and hence will get stale if
+ descriptors aren't periodically updated.
+
+ Arguments:
+ default - result if the query fails
+ """
+
+ return self._getRelayAttr("bwObserved", default)
+
+ def getMyBandwidthMeasured(self, default = None):
+ """
+ Provides the relay's current measured bandwidth (the throughput as noted by
+ the directory authorities and used by clients for relay selection). This is
+ undefined if not in the consensus or with older versions of Tor. Depending
+ on the circumstances this can be from a variety of things (observed,
+ measured, weighted measured, etc) as described by:
+ https://trac.torproject.org/projects/tor/ticket/1566
+
+ Arguments:
+ default - result if the query fails
+ """
+
+ return self._getRelayAttr("bwMeasured", default)
+
+ def getMyFingerprint(self, default = None):
+ """
+ Provides the fingerprint for this relay.
+
+ Arguments:
+ default - result if the query fails
+ """
+
+ return self._getRelayAttr("fingerprint", default, False)
+
+ def getMyFlags(self, default = None):
+ """
+ Provides the flags held by this relay.
+
+ Arguments:
+ default - result if the query fails or this relay isn't a part of the consensus yet
+ """
+
+ return self._getRelayAttr("flags", default)
+
+ def getMyPid(self):
+ """
+ Provides the pid of the attached tor process (None if no controller exists
+ or this can't be determined).
+ """
+
+ return self._getRelayAttr("pid", None)
+
+ def getStatus(self):
+ """
+ Provides a tuple consisting of the control port's current status and unix
+ time-stamp for when it became this way (zero if no status has yet to be
+ set).
+ """
+
+ return (self._status, self._statusTime)
+
+ def addEventListener(self, listener):
+ """
+ Directs further tor controller events to callback functions of the
+ listener. If a new control connection is initialized then this listener is
+ reattached.
+
+ Arguments:
+ listener - TorCtl.PostEventListener instance listening for events
+ """
+
+ self.connLock.acquire()
+ self.eventListeners.append(listener)
+ if self.isAlive(): self.conn.add_event_listener(listener)
+ self.connLock.release()
+
+ def addTorCtlListener(self, callback):
+ """
+ Directs further TorCtl events to the callback function. Events are composed
+ of a runlevel and message tuple.
+
+ Arguments:
+ callback - functor that'll accept the events, expected to be of the form:
+ myFunction(runlevel, msg)
+ """
+
+ self.torctlListeners.append(callback)
+
+ def addStatusListener(self, callback):
+ """
+ Directs further events related to tor's controller status to the callback
+ function.
+
+ Arguments:
+ callback - functor that'll accept the events, expected to be of the form:
+ myFunction(controller, eventType)
+ """
+
+ self.statusListeners.append(callback)
+
+ def removeStatusListener(self, callback):
+ """
+ Stops listener from being notified of further events. This returns true if a
+ listener's removed, false otherwise.
+
+ Arguments:
+ callback - functor to be removed
+ """
+
+ if callback in self.statusListeners:
+ self.statusListeners.remove(callback)
+ return True
+ else: return False
+
+ def getControllerEvents(self):
+ """
+ Provides the events the controller's currently configured to listen for.
+ """
+
+ return list(self.controllerEvents)
+
+ def setControllerEvents(self, events):
+ """
+ Sets the events being requested from any attached tor instance, logging
+ warnings for event types that aren't supported (possibly due to version
+ issues). Events in REQ_EVENTS will also be included, logging at the error
+ level with an additional description in case of failure.
+
+ This remembers the successfully set events and tries to request them from
+ any tor instance it attaches to in the future too (again logging and
+ dropping unsuccessful event types).
+
+ This returns the listing of event types that were successfully set. If not
+ currently attached to a tor instance then all events are assumed to be ok,
+ then attempted when next attached to a control port.
+
+ Arguments:
+ events - listing of events to be set
+ """
+
+ self.connLock.acquire()
+
+ returnVal = []
+ if self.isAlive():
+ events = set(events)
+ events = events.union(set(REQ_EVENTS.keys()))
+ unavailableEvents = set()
+
+ # removes anything we've already failed to set
+ if DROP_FAILED_EVENTS:
+ unavailableEvents.update(events.intersection(FAILED_EVENTS))
+ events.difference_update(FAILED_EVENTS)
+
+ # initial check for event availability, using the 'events/names' GETINFO
+ # option to detect invalid events
+ validEvents = self.getInfo("events/names")
+
+ if validEvents:
+ validEvents = set(validEvents.split())
+ unavailableEvents.update(events.difference(validEvents))
+ events.intersection_update(validEvents)
+
+ # attempt to set events via trial and error
+ isEventsSet, isAbandoned = False, False
+
+ while not isEventsSet and not isAbandoned:
+ try:
+ self.conn.set_events(list(events))
+ isEventsSet = True
+ except TorCtl.ErrorReply, exc:
+ msg = str(exc)
+
+ if "Unrecognized event" in msg:
+ # figure out type of event we failed to listen for
+ start = msg.find("event \"") + 7
+ end = msg.rfind("\"")
+ failedType = msg[start:end]
+
+ unavailableEvents.add(failedType)
+ events.discard(failedType)
+ else:
+ # unexpected error, abandon attempt
+ isAbandoned = True
+ except TorCtl.TorCtlClosed:
+ self.close()
+ isAbandoned = True
+
+ FAILED_EVENTS.update(unavailableEvents)
+ if not isAbandoned:
+ # logs warnings or errors for failed events
+ for eventType in unavailableEvents:
+ defaultMsg = DEFAULT_FAILED_EVENT_MSG % eventType
+ if eventType in REQ_EVENTS:
+ log.log(log.ERR, defaultMsg + " (%s)" % REQ_EVENTS[eventType])
+ else:
+ log.log(log.WARN, defaultMsg)
+
+ self.controllerEvents = list(events)
+ returnVal = list(events)
+ else:
+ # attempts to set the events when next attached to a control port
+ self.controllerEvents = list(events)
+ returnVal = list(events)
+
+ self.connLock.release()
+ return returnVal
+
+ def reload(self, issueSighup = False):
+ """
+ This resets tor (sending a RELOAD signal to the control port) causing tor's
+ internal state to be reset and the torrc reloaded. This can either be done
+ by...
+ - the controller via a RELOAD signal (default and suggested)
+ conn.send_signal("RELOAD")
+ - system reload signal (hup)
+ pkill -sighup tor
+
+ The later isn't really useful unless there's some reason the RELOAD signal
+ won't do the trick. Both methods raise an IOError in case of failure.
+
+ Arguments:
+ issueSighup - issues a sighup rather than a controller RELOAD signal
+ """
+
+ self.connLock.acquire()
+
+ raisedException = None
+ if self.isAlive():
+ if not issueSighup:
+ try:
+ self.conn.send_signal("RELOAD")
+ except Exception, exc:
+ # new torrc parameters caused an error (tor's likely shut down)
+ # BUG: this doesn't work - torrc errors still cause TorCtl to crash... :(
+ # http://bugs.noreply.org/flyspray/index.php?do=details&id=1329
+ raisedException = IOError(str(exc))
+ else:
+ try:
+ # Redirects stderr to stdout so we can check error status (output
+ # should be empty if successful). Example error:
+ # pkill: 5592 - Operation not permitted
+ #
+ # note that this may provide multiple errors, even if successful,
+ # hence this:
+ # - only provide an error if Tor fails to log a sighup
+ # - provide the error message associated with the tor pid (others
+ # would be a red herring)
+ if not sysTools.isAvailable("pkill"):
+ raise IOError("pkill command is unavailable")
+
+ self._isReset = False
+ pkillCall = os.popen("pkill -sighup ^tor$ 2> /dev/stdout")
+ pkillOutput = pkillCall.readlines()
+ pkillCall.close()
+
+ # Give the sighupTracker a moment to detect the sighup signal. This
+ # is, of course, a possible concurrency bug. However I'm not sure
+ # of a better method for blocking on this...
+ waitStart = time.time()
+ while time.time() - waitStart < 1:
+ time.sleep(0.1)
+ if self._isReset: break
+
+ if not self._isReset:
+ errorLine, torPid = "", self.getMyPid()
+ if torPid:
+ for line in pkillOutput:
+ if line.startswith("pkill: %s - " % torPid):
+ errorLine = line
+ break
+
+ if errorLine: raise IOError(" ".join(errorLine.split()[3:]))
+ else: raise IOError("failed silently")
+ except IOError, exc:
+ raisedException = exc
+
+ self.connLock.release()
+
+ if raisedException: raise raisedException
+
+ def msg_event(self, event):
+ """
+ Listens for reload signal (hup), which is either produced by:
+ causing the torrc and internal state to be reset.
+ """
+
+ if event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)"):
+ self._isReset = True
+
+ self._status = TOR_INIT
+ self._statusTime = time.time()
+
+ thread.start_new_thread(self._notifyStatusListeners, (TOR_INIT,))
+
+ def ns_event(self, event):
+ self._updateHeartbeat()
+
+ myFingerprint = self.getMyFingerprint()
+ if myFingerprint:
+ for ns in event.nslist:
+ if ns.idhex == myFingerprint:
+ self._cachedParam["nsEntry"] = None
+ self._cachedParam["flags"] = None
+ self._cachedParam["bwMeasured"] = None
+ return
+ else:
+ self._cachedParam["nsEntry"] = None
+ self._cachedParam["flags"] = None
+ self._cachedParam["bwMeasured"] = None
+
+ def new_consensus_event(self, event):
+ self._updateHeartbeat()
+
+ self._cachedParam["nsEntry"] = None
+ self._cachedParam["flags"] = None
+ self._cachedParam["bwMeasured"] = None
+
+ def new_desc_event(self, event):
+ self._updateHeartbeat()
+
+ myFingerprint = self.getMyFingerprint()
+ if not myFingerprint or myFingerprint in event.idlist:
+ self._cachedParam["descEntry"] = None
+ self._cachedParam["bwObserved"] = None
+
+ def circ_status_event(self, event):
+ self._updateHeartbeat()
+
+ def buildtimeout_set_event(self, event):
+ self._updateHeartbeat()
+
+ def stream_status_event(self, event):
+ self._updateHeartbeat()
+
+ def or_conn_status_event(self, event):
+ self._updateHeartbeat()
+
+ def stream_bw_event(self, event):
+ self._updateHeartbeat()
+
+ def bandwidth_event(self, event):
+ self._updateHeartbeat()
+
+ def address_mapped_event(self, event):
+ self._updateHeartbeat()
+
+ def unknown_event(self, event):
+ self._updateHeartbeat()
+
+ def write(self, msg):
+ """
+ Tracks TorCtl events. Ugly hack since TorCtl/TorUtil.py expects a file.
+ """
+
+ timestampStart, timestampEnd = msg.find("["), msg.find("]")
+ level = msg[:timestampStart]
+ msg = msg[timestampEnd + 2:].strip()
+
+ # notifies listeners of TorCtl events
+ for callback in self.torctlListeners: callback(level, msg)
+
+ # checks if TorCtl is providing a notice that control port is closed
+ if TOR_CTL_CLOSE_MSG in msg: self.close()
+
+ def flush(self): pass
+
+ def _updateHeartbeat(self):
+ """
+ Called on any event occurance to note the time it occured.
+ """
+
+ # alternative is to use the event's timestamp (via event.arrived_at)
+ self.lastHeartbeat = time.time()
+
+ def _getRelayAttr(self, key, default, cacheUndefined = True):
+ """
+ Provides information associated with this relay, using the cached value if
+ available and otherwise looking it up.
+
+ Arguments:
+ key - parameter being queried (from CACHE_ARGS)
+ default - value to be returned if undefined
+ cacheUndefined - caches when values are undefined, avoiding further
+ lookups if true
+ """
+
+ currentVal = self._cachedParam[key]
+ if currentVal:
+ if currentVal == UNKNOWN: return default
+ else: return currentVal
+
+ self.connLock.acquire()
+
+ currentVal, result = self._cachedParam[key], None
+ if not currentVal and self.isAlive():
+ # still unset - fetch value
+ if key in ("nsEntry", "descEntry"):
+ myFingerprint = self.getMyFingerprint()
+
+ if myFingerprint:
+ queryType = "ns" if key == "nsEntry" else "desc"
+ queryResult = self.getInfo("%s/id/%s" % (queryType, myFingerprint))
+ if queryResult: result = queryResult.split("\n")
+ elif key == "bwRate":
+ # effective relayed bandwidth is the minimum of BandwidthRate,
+ # MaxAdvertisedBandwidth, and RelayBandwidthRate (if set)
+ effectiveRate = int(self.getOption("BandwidthRate"))
+
+ relayRate = self.getOption("RelayBandwidthRate")
+ if relayRate and relayRate != "0":
+ effectiveRate = min(effectiveRate, int(relayRate))
+
+ maxAdvertised = self.getOption("MaxAdvertisedBandwidth")
+ if maxAdvertised: effectiveRate = min(effectiveRate, int(maxAdvertised))
+
+ result = effectiveRate
+ elif key == "bwBurst":
+ # effective burst (same for BandwidthBurst and RelayBandwidthBurst)
+ effectiveBurst = int(self.getOption("BandwidthBurst"))
+
+ relayBurst = self.getOption("RelayBandwidthBurst")
+ if relayBurst and relayBurst != "0":
+ effectiveBurst = min(effectiveBurst, int(relayBurst))
+
+ result = effectiveBurst
+ elif key == "bwObserved":
+ for line in self.getMyDescriptor([]):
+ if line.startswith("bandwidth"):
+ # line should look something like:
+ # bandwidth 40960 102400 47284
+ comp = line.split()
+
+ if len(comp) == 4 and comp[-1].isdigit():
+ result = int(comp[-1])
+ break
+ elif key == "bwMeasured":
+ # TODO: Currently there's no client side indication of what type of
+ # measurement was used. Include this in results if it's ever available.
+
+ for line in self.getMyNetworkStatus([]):
+ if line.startswith("w Bandwidth="):
+ bwValue = line[12:]
+ if bwValue.isdigit(): result = int(bwValue)
+ break
+ elif key == "fingerprint":
+ # Fingerprints are kept until sighup if set (most likely not even a
+ # setconf can change it since it's in the data directory). If orport is
+ # unset then no fingerprint will be set.
+ orPort = self.getOption("ORPort", "0")
+ if orPort == "0": result = UNKNOWN
+ else: result = self.getInfo("fingerprint")
+ elif key == "flags":
+ for line in self.getMyNetworkStatus([]):
+ if line.startswith("s "):
+ result = line[2:].split()
+ break
+ elif key == "pid":
+ result = getPid(int(self.getOption("ControlPort", 9051)), self.getOption("PidFile"))
+
+ # cache value
+ if result: self._cachedParam[key] = result
+ elif cacheUndefined: self._cachedParam[key] = UNKNOWN
+ elif currentVal == UNKNOWN: result = currentVal
+
+ self.connLock.release()
+
+ if result: return result
+ else: return default
+
+ def _notifyStatusListeners(self, eventType):
+ """
+ Sends a notice to all current listeners that a given change in tor's
+ controller status has occurred.
+
+ Arguments:
+ eventType - enum representing tor's new status
+ """
+
+ # resets cached getInfo parameters
+ self._cachedParam = dict([(arg, "") for arg in CACHE_ARGS])
+
+ # gives a notice that the control port has closed
+ if eventType == TOR_CLOSED:
+ log.log(CONFIG["log.torCtlPortClosed"], "Tor control port closed")
+
+ for callback in self.statusListeners:
+ callback(self, eventType)
+
More information about the tor-commits
mailing list