[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r20198: {arm} Work done over the trip. added: customizable update interval (in arm/trunk: . interface)
Author: atagar
Date: 2009-07-30 03:09:47 -0400 (Thu, 30 Jul 2009)
New Revision: 20198
Added:
arm/trunk/screenshot_page1.png
arm/trunk/screenshot_page2.png
Removed:
arm/trunk/arm-in-action.png
Modified:
arm/trunk/arm.py
arm/trunk/interface/bandwidthPanel.py
arm/trunk/interface/confPanel.py
arm/trunk/interface/connPanel.py
arm/trunk/interface/controller.py
arm/trunk/interface/headerPanel.py
arm/trunk/interface/hostnameResolver.py
arm/trunk/interface/logPanel.py
arm/trunk/interface/util.py
arm/trunk/readme.txt
Log:
Work done over the trip.
added: customizable update interval for bandwidth graph (feature request by StrangeCharm)
change: noted new project page in the readme (www.atagar.com/arm)
change: added word wrapping to conf panel
change: added function for custom popup menus
change: logs error message when required event types are unsupported rather than throwing an exception
change: using different screenshot images
fix: resolved issue that caused monitor to think tor was resumed when quit
fix: bug with panel utility's resize detection
fix: resorts connections after NEWDESC and NEWCONSENSUS events
fix: forgetting to to resume monitor at multiple points after a temporary pause
fix: minor refactoring based on suggestions from pylint (unused imports and such)
Deleted: arm/trunk/arm-in-action.png
===================================================================
(Binary files differ)
Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/arm.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -12,7 +12,6 @@
import os
import socket
import getpass
-import binascii
from TorCtl import TorCtl
from TorCtl import TorUtil
@@ -28,7 +27,7 @@
NO_AUTH, COOKIE_AUTH, PASSWORD_AUTH = range(3) # enums for authentication type
HELP_TEXT = """Usage arm [OPTION]
-Terminal Tor relay status monitor.
+Terminal status monitor for Tor relays.
-i, --interface [ADDRESS:]PORT change control interface from %s:%i
-c, --cookie[=PATH] authenticates using cookie, PATH defaults to
Modified: arm/trunk/interface/bandwidthPanel.py
===================================================================
--- arm/trunk/interface/bandwidthPanel.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/bandwidthPanel.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -3,21 +3,26 @@
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
import time
+import copy
import curses
from TorCtl import TorCtl
import util
-BANDWIDTH_GRAPH_SAMPLES = 5 # seconds of data used for a bar in the graph
BANDWIDTH_GRAPH_COL = 30 # columns of data in graph
BANDWIDTH_GRAPH_COLOR_DL = "green" # download section color
BANDWIDTH_GRAPH_COLOR_UL = "cyan" # upload section color
+# time intervals at which graphs can be updated
+DEFAULT_INTERVAL_INDEX = 1 # defaults to using five seconds of data per bar in the graph
+UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5), ("30 seconds", 30), ("minutely", 60),
+ ("half hour", 1800), ("hourly", 3600), ("daily", 86400)]
+
class BandwidthMonitor(TorCtl.PostEventListener, util.Panel):
"""
Tor event listener, taking bandwidth sampling and drawing bar graph. This is
updated every second by the BW events and graph samples are spaced at
- BANDWIDTH_GRAPH_SAMPLES second intervals.
+ a timescale determined by the updateIntervalIndex.
"""
def __init__(self, lock, conn):
@@ -32,17 +37,24 @@
self.tick = 0 # number of updates performed
self.lastDownloadRate = 0 # most recently sampled rates
self.lastUploadRate = 0
- self.maxDownloadRate = 1 # max rates seen, used to determine graph bounds
- self.maxUploadRate = 1
self.accountingInfo = None # accounting data (set by _updateAccountingInfo method)
self.isPaused = False
self.isVisible = True
+ self.showLabel = True # shows top label if true, hides otherwise
self.pauseBuffer = None # mirror instance used to track updates when paused
+ self.updateIntervalIndex = DEFAULT_INTERVAL_INDEX
# graphed download (read) and upload (write) rates - first index accumulator
- self.downloadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
- self.uploadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
+ # iterative insert is to avoid issue with shallow copies (nasty, nasty gotcha)
+ self.downloadRates, self.uploadRates = [], []
+ for i in range(len(UPDATE_INTERVALS)):
+ self.downloadRates.insert(0, (BANDWIDTH_GRAPH_COL + 1) * [0])
+ self.uploadRates.insert(0, (BANDWIDTH_GRAPH_COL + 1) * [0])
+ # max rates seen, used to determine graph bounds
+ self.maxDownloadRate = len(UPDATE_INTERVALS) * [1]
+ self.maxUploadRate = len(UPDATE_INTERVALS) * [1]
+
# used to calculate averages, uses tick for time
self.totalDownload = 0
self.totalUpload = 0
@@ -60,21 +72,24 @@
self.lastDownloadRate = event.read
self.lastUploadRate = event.written
- self.downloadRates[0] += event.read
- self.uploadRates[0] += event.written
-
self.totalDownload += event.read
self.totalUpload += event.written
+ # updates graphs for all time intervals
self.tick += 1
- if self.tick % BANDWIDTH_GRAPH_SAMPLES == 0:
- self.maxDownloadRate = max(self.maxDownloadRate, self.downloadRates[0])
- self.downloadRates.insert(0, 0)
- del self.downloadRates[BANDWIDTH_GRAPH_COL + 1:]
+ for i in range(len(UPDATE_INTERVALS)):
+ self.downloadRates[i][0] += event.read
+ self.uploadRates[i][0] += event.written
+ interval = UPDATE_INTERVALS[i][1]
- self.maxUploadRate = max(self.maxUploadRate, self.uploadRates[0])
- self.uploadRates.insert(0, 0)
- del self.uploadRates[BANDWIDTH_GRAPH_COL + 1:]
+ if self.tick % interval == 0:
+ self.maxDownloadRate[i] = max(self.maxDownloadRate[i], self.downloadRates[i][0] / interval)
+ self.downloadRates[i].insert(0, 0)
+ del self.downloadRates[i][BANDWIDTH_GRAPH_COL + 1:]
+
+ self.maxUploadRate[i] = max(self.maxUploadRate[i], self.uploadRates[i][0] / interval)
+ self.uploadRates[i].insert(0, 0)
+ del self.uploadRates[i][BANDWIDTH_GRAPH_COL + 1:]
self.redraw()
@@ -94,29 +109,29 @@
labelContents = "%s):" % labelContents[:labelContents.find(",")] # removes burst measure
if self.maxX < len(labelContents): labelContents = "Bandwidth:" # removes both
- self.addstr(0, 0, labelContents, util.LABEL_ATTR)
+ if self.showLabel: self.addstr(0, 0, labelContents, util.LABEL_ATTR)
# current numeric measures
self.addstr(1, 0, "Downloaded (%s/sec):" % util.getSizeLabel(self.lastDownloadRate), curses.A_BOLD | dlColor)
self.addstr(1, 35, "Uploaded (%s/sec):" % util.getSizeLabel(self.lastUploadRate), curses.A_BOLD | ulColor)
# graph bounds in KB (uses highest recorded value as max)
- self.addstr(2, 0, "%4s" % str(self.maxDownloadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES), dlColor)
+ self.addstr(2, 0, "%4s" % str(self.maxDownloadRate[self.updateIntervalIndex] / 1024), dlColor)
self.addstr(7, 0, " 0", dlColor)
- self.addstr(2, 35, "%4s" % str(self.maxUploadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES), ulColor)
+ self.addstr(2, 35, "%4s" % str(self.maxUploadRate[self.updateIntervalIndex] / 1024), ulColor)
self.addstr(7, 35, " 0", ulColor)
# creates bar graph of bandwidth usage over time
for col in range(BANDWIDTH_GRAPH_COL):
- bytesDownloaded = self.downloadRates[col + 1]
- colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate)
+ bytesDownloaded = self.downloadRates[self.updateIntervalIndex][col + 1] / UPDATE_INTERVALS[self.updateIntervalIndex][1]
+ colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate[self.updateIntervalIndex])
for row in range(colHeight):
self.addstr(7 - row, col + 5, " ", curses.A_STANDOUT | dlColor)
for col in range(BANDWIDTH_GRAPH_COL):
- bytesUploaded = self.uploadRates[col + 1]
- colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate)
+ bytesUploaded = self.uploadRates[self.updateIntervalIndex][col + 1] / UPDATE_INTERVALS[self.updateIntervalIndex][1]
+ colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate[self.updateIntervalIndex])
for row in range(colHeight):
self.addstr(7 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
@@ -149,6 +164,16 @@
finally:
self.lock.release()
+ def setUpdateInterval(self, intervalIndex):
+ """
+ Sets the timeframe at which the graph is updated. This throws a ValueError
+ if the index isn't within UPDATE_INTERVALS.
+ """
+
+ if intervalIndex >= 0 and intervalIndex < len(UPDATE_INTERVALS):
+ self.updateIntervalIndex = intervalIndex
+ else: raise ValueError("%i out of bounds of UPDATE_INTERVALS" % intervalIndex)
+
def setPaused(self, isPause):
"""
If true, prevents bandwidth updates from being presented.
@@ -175,13 +200,14 @@
if self.isPaused or not self.isVisible:
if self.pauseBuffer == None: self.pauseBuffer = BandwidthMonitor(None, None)
+ # TODO: use a more clever swap using 'active' and 'inactive' instances
self.pauseBuffer.tick = self.tick
self.pauseBuffer.lastDownloadRate = self.lastDownloadRate
- self.pauseBuffer.lastuploadRate = self.lastUploadRate
- self.pauseBuffer.maxDownloadRate = self.maxDownloadRate
- self.pauseBuffer.maxUploadRate = self.maxUploadRate
- self.pauseBuffer.downloadRates = list(self.downloadRates)
- self.pauseBuffer.uploadRates = list(self.uploadRates)
+ self.pauseBuffer.lastUploadRate = self.lastUploadRate
+ self.pauseBuffer.maxDownloadRate = list(self.maxDownloadRate)
+ self.pauseBuffer.maxUploadRate = list(self.maxUploadRate)
+ self.pauseBuffer.downloadRates = copy.deepcopy(self.downloadRates)
+ self.pauseBuffer.uploadRates = copy.deepcopy(self.uploadRates)
self.pauseBuffer.totalDownload = self.totalDownload
self.pauseBuffer.totalUpload = self.totalUpload
self.pauseBuffer.bwRate = self.bwRate
@@ -189,7 +215,7 @@
else:
self.tick = self.pauseBuffer.tick
self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
- self.lastUploadRate = self.pauseBuffer.lastuploadRate
+ self.lastUploadRate = self.pauseBuffer.lastUploadRate
self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
self.maxUploadRate = self.pauseBuffer.maxUploadRate
self.downloadRates = self.pauseBuffer.downloadRates
Modified: arm/trunk/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/confPanel.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -4,7 +4,6 @@
import math
import curses
-from TorCtl import TorCtl
import util
@@ -18,6 +17,8 @@
self.confLocation = confLocation
self.showLineNum = True
self.stripComments = False
+ self.confContents = []
+ self.scroll = 0
self.reset()
def reset(self):
@@ -65,26 +66,54 @@
pageHeight = self.maxY - 1
numFieldWidth = int(math.log10(len(displayText))) + 1
+ lineNum = 1
for i in range(self.scroll, min(len(displayText), self.scroll + pageHeight)):
lineText = displayText[i].strip()
- endBreak = 0
+ numOffset = 0 # offset for line numbering
if self.showLineNum:
- self.addstr(i - self.scroll + 1, 0, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | util.getColor("yellow"))
+ self.addstr(lineNum, 0, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | util.getColor("yellow"))
numOffset = numFieldWidth + 1
- else: numOffset = 0
- if not lineText: continue
- elif not lineText[0] == "#":
- ctlBreak = lineText.find(" ")
- endBreak = lineText.find("#")
- if endBreak == -1: endBreak = len(lineText)
+ command, argument, comment = "", "", ""
+ if not lineText: continue # no text
+ elif lineText[0] == "#":
+ # whole line is commented out
+ comment = lineText
+ 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)
- self.addstr(i - self.scroll + 1, numOffset, lineText[:ctlBreak], curses.A_BOLD | util.getColor("green"))
- self.addstr(i - self.scroll + 1, numOffset + ctlBreak, lineText[ctlBreak:endBreak], curses.A_BOLD | util.getColor("cyan"))
- self.addstr(i - self.scroll + 1, numOffset + endBreak, lineText[endBreak:], util.getColor("white"))
-
+ command, argument, comment = lineText[:ctlEnd], lineText[ctlEnd:argEnd], lineText[argEnd:]
+
+ xLoc = 0
+ lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, numOffset, command, curses.A_BOLD | util.getColor("green"))
+ lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, numOffset, argument, curses.A_BOLD | util.getColor("cyan"))
+ lineNum, xLoc = self.addstr_wrap(lineNum, xLoc, numOffset, comment, util.getColor("white"))
+ lineNum += 1
+
self.refresh()
finally:
self.lock.release()
+
+ def addstr_wrap(self, y, x, indent, text, formatting):
+ """
+ Writes text with word wrapping, returning the ending y/x coordinate.
+ """
+
+ if not text: return (y, x) # nothing to write
+ lineWidth = self.maxX - indent # room for text
+ while True:
+ if len(text) > lineWidth - x - 1:
+ chunkSize = text.rfind(" ", 0, lineWidth - x)
+ writeText = text[:chunkSize]
+ text = text[chunkSize:].strip()
+
+ self.addstr(y, x + indent, writeText, formatting)
+ y, x = y + 1, 0
+ else:
+ self.addstr(y, x + indent, text, formatting)
+ return (y, x + len(text))
Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/connPanel.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -14,6 +14,13 @@
LIST_IP, LIST_HOSTNAME, LIST_FINGERPRINT, LIST_NICKNAME = range(4)
LIST_LABEL = {LIST_IP: "IP Address", LIST_HOSTNAME: "Hostname", LIST_FINGERPRINT: "Fingerprint", LIST_NICKNAME: "Nickname"}
+# attributes for connection types
+TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red"}
+TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2} # defines ordering
+
+# enums for indexes of ConnPanel 'connections' fields
+CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY = range(6)
+
# enums for sorting types (note: ordering corresponds to SORT_TYPES for easy lookup)
# TODO: add ORD_BANDWIDTH -> (ORD_BANDWIDTH, "Bandwidth", lambda x, y: ???)
ORD_TYPE, ORD_FOREIGN_LISTING, ORD_SRC_LISTING, ORD_DST_LISTING, ORD_COUNTRY, ORD_FOREIGN_PORT, ORD_SRC_PORT, ORD_DST_PORT = range(8)
@@ -31,12 +38,6 @@
(ORD_DST_PORT, "Port (Dest.)",
lambda x, y: int(x[CONN_L_PORT] if x[CONN_TYPE] == "inbound" else x[CONN_F_PORT]) - int(y[CONN_L_PORT] if y[CONN_TYPE] == "inbound" else y[CONN_F_PORT]))]
-TYPE_COLORS = {"inbound": "green", "outbound": "blue", "control": "red"}
-TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2}
-
-# enums for indexes of ConnPanel 'connections' fields
-CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY = range(6)
-
# provides bi-directional mapping of sorts with their associated labels
def getSortLabel(sortType, withColor = False):
"""
@@ -123,6 +124,7 @@
self.fingerprintLookupCache.clear()
self.nicknameLookupCache.clear()
self.fingerprintMappings = _getFingerprintMappings(self.conn, event.nslist)
+ if self.listingType != LIST_HOSTNAME: self.sortConnections()
def new_desc_event(self, event):
for fingerprint in event.idlist:
@@ -157,6 +159,7 @@
self.fingerprintMappings[nsEntry.ip].append((nsEntry.orport, nsEntry.idhex))
else:
self.fingerprintMappings[nsEntry.ip] = [(nsEntry.orport, nsEntry.idhex)]
+ if self.listingType != LIST_HOSTNAME: self.sortConnections()
def reset(self):
"""
Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/controller.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -34,7 +34,6 @@
["torrc"]]
PAUSEABLE = ["header", "bandwidth", "log", "conn"]
PAGE_COUNT = 3 # all page numbering is internally represented as 0-indexed
-# TODO: page for configuration information
# events needed for panels other than the event log
REQ_EVENTS = ["BW", "NEWDESC", "NEWCONSENSUS"]
@@ -92,6 +91,64 @@
self.addstr(0, 0, msgText, msgAttr)
self.refresh()
+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 popup.lock.acquire(False): return -1
+ try:
+ 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, popup.startY, newWidth)
+
+ key = 0
+ while key not in (curses.KEY_ENTER, 10, ord(' ')):
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, title, util.LABEL_ATTR)
+
+ 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, popup.startY, 80)
+
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ cursesLock.release()
+
+ return selection
+
def setEventListening(loggedEvents, conn, logListener):
"""
Tries to set events being listened for, displaying error for any event
@@ -100,12 +157,14 @@
"""
eventsSet = False
+ # adds events used for panels to function if not already included
+ connEvents = loggedEvents.union(set(REQ_EVENTS))
+
+ # removes UNKNOWN since not an actual event type
+ connEvents.discard("UNKNOWN")
+
while not eventsSet:
try:
- # adds BW events if not already included (so bandwidth monitor will work)
- # removes UNKNOWN since not an actual event type
- connEvents = loggedEvents.union(set(REQ_EVENTS))
- connEvents.discard("UNKNOWN")
conn.set_events(connEvents)
eventsSet = True
except TorCtl.ErrorReply, exc:
@@ -115,13 +174,17 @@
start = msg.find("event \"") + 7
end = msg.rfind("\"")
eventType = msg[start:end]
- if eventType == "BW": raise exc # bandwidth monitoring won't work - best to crash
# removes and notes problem
- loggedEvents.remove(eventType)
- logListener.monitor_event("WARN", "Unsupported event type: %s" % eventType)
- else:
- raise exc
+ connEvents.discard(eventType)
+ if eventType in loggedEvents: loggedEvents.remove(eventType)
+
+ if eventType in REQ_EVENTS:
+ if eventType == "BW": msg = "(bandwidth panel won't function)"
+ elif eventType in ("NEWDESC", "NEWCONSENSUS"): msg = "(connections listing can't register consensus changes)"
+ else: msg = ""
+ logListener.monitor_event("ERR", "Unsupported event type: %s %s" % (eventType, msg))
+ else: logListener.monitor_event("WARN", "Unsupported event type: %s" % eventType)
except TorCtl.TorCtlClosed:
return []
@@ -226,10 +289,10 @@
if not isUnresponsive and panels["log"].getHeartbeat() >= 10:
isUnresponsive = True
panels["log"].monitor_event("NOTICE", "Relay unresponsive (last heartbeat: %s)" % time.ctime(panels["log"].lastHeartbeat))
- elif isUnresponsive and panels["log"].getHeartbeat() < 5:
- # this really shouldn't happen - BW events happen every second...
+ elif isUnresponsive and panels["log"].getHeartbeat() < 10:
+ # shouldn't happen unless Tor freezes for a bit - BW events happen every second...
isUnresponsive = False
- panels["log"].monitor_event("WARN", "Relay resumed")
+ panels["log"].monitor_event("NOTICE", "Relay resumed")
# if it's been at least five seconds since the last refresh of connection listing, update
currentTime = time.time()
@@ -253,7 +316,7 @@
# pauses panels that aren't visible to prevent events from accumilating
# (otherwise they'll wait on the curses lock which might get demanding)
- for key in PAUSEABLE: panels[key].setPaused(isPaused or (key not in PAGES[page] and key not in PAGE_S))
+ setPauseState(panels, isPaused, page)
panels["control"].page = page + 1
panels["control"].refresh()
@@ -262,7 +325,7 @@
cursesLock.acquire()
try:
isPaused = not isPaused
- for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
+ setPauseState(panels, isPaused, page)
panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
finally:
cursesLock.release()
@@ -270,7 +333,7 @@
# displays popup for current page's controls
cursesLock.acquire()
try:
- for key in PAUSEABLE: panels[key].setPaused(True)
+ setPauseState(panels, isPaused, page, True)
# lists commands
popup = panels["popup"]
@@ -281,7 +344,11 @@
if page == 0:
bwVisibleLabel = "visible" if panels["bandwidth"].isVisible else "hidden"
popup.addfstr(1, 2, "b: toggle bandwidth panel (<b>%s</b>)" % bwVisibleLabel)
- popup.addstr(1, 41, "e: change logged events")
+
+ # matches timescale used by bandwith panel to recognized labeling
+ intervalLabel = bandwidthPanel.UPDATE_INTERVALS[panels["bandwidth"].updateIntervalIndex][0]
+ popup.addfstr(1, 41, "i: graph update interval (<b>%s</b>)" % intervalLabel)
+ popup.addstr(2, 2, "e: change logged events")
if page == 1:
popup.addstr(1, 2, "up arrow: scroll up a line")
popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -317,18 +384,37 @@
stdscr.getch()
curses.halfdelay(REFRESH_RATE * 10)
- for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
+ setPauseState(panels, isPaused, page)
finally:
cursesLock.release()
elif page == 0 and (key == ord('b') or key == ord('B')):
# toggles bandwidth panel visability
panels["bandwidth"].setVisible(not panels["bandwidth"].isVisible)
oldY = -1 # force resize event
+ elif page == 0 and (key == ord('i') or key == ord('I')):
+ # provides menu to pick bandwidth graph update interval
+ options = [label for (label, intervalTime) in bandwidthPanel.UPDATE_INTERVALS]
+ initialSelection = panels["bandwidth"].updateIntervalIndex
+
+ # hides top label of bandwidth panel and pauses panels
+ panels["bandwidth"].showLabel = False
+ panels["bandwidth"].redraw()
+ setPauseState(panels, isPaused, page, True)
+
+ selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
+
+ # reverts changes made for popup
+ panels["bandwidth"].showLabel = True
+ setPauseState(panels, isPaused, page)
+
+ # applies new setting
+ if selection != -1:
+ panels["bandwidth"].setUpdateInterval(selection)
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
cursesLock.acquire()
try:
- for key in PAUSEABLE: panels[key].setPaused(True)
+ setPauseState(panels, isPaused, page, True)
# provides prompt
panels["control"].setMsg("Events to log: ")
@@ -360,7 +446,7 @@
curses.noecho()
curses.halfdelay(REFRESH_RATE * 10) # evidenlty previous tweaks reset this...
- # TODO: it would be nice to quit on esc, but looks like this might not be possible...
+ # it would be nice to quit on esc, but looks like this might not be possible...
if eventsInput != "":
try:
expandedEvents = logPanel.expandEvents(eventsInput)
@@ -372,7 +458,7 @@
time.sleep(2)
panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
+ setPauseState(panels, isPaused, page)
finally:
cursesLock.release()
elif key == 27 and panels["conn"].listingType == connPanel.LIST_HOSTNAME and panels["control"].resolvingCounter != -1:
@@ -383,80 +469,42 @@
panels["conn"].sortConnections()
elif page == 1 and (key == ord('l') or key == ord('L')):
# provides menu to pick identification info listed for connections
- cursesLock.acquire()
- try:
- for key in PAUSEABLE: panels[key].setPaused(True)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
- popup = panels["popup"]
+ 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()
+ 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]
- # uses smaller dimentions more fitting for small content
- panels["popup"].height = 6
- panels["popup"].recreate(stdscr, startY, 20)
-
- # hides top label of conn panel
- panels["conn"].showLabel = False
- panels["conn"].redraw()
-
- selection = panels["conn"].listingType # starts with current option selected
- options = [connPanel.LIST_IP, connPanel.LIST_HOSTNAME, connPanel.LIST_FINGERPRINT, connPanel.LIST_NICKNAME]
- key = 0
-
- while key not in (curses.KEY_ENTER, 10, ord(' ')):
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "List By:", util.LABEL_ATTR)
+ if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
+ curses.halfdelay(10) # refreshes display every second until done resolving
+ panels["control"].resolvingCounter = panels["conn"].resolver.totalResolves - panels["conn"].resolver.unresolvedQueue.qsize()
- for i in range(len(options)):
- sortType = options[i]
- format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
-
- if panels["conn"].listingType == sortType: tab = "> "
- else: tab = " "
- sortLabel = connPanel.LIST_LABEL[sortType]
-
- popup.addstr(i + 1, 2, tab)
- popup.addstr(i + 1, 4, sortLabel, 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:
- # esc - cancel
- selection = panels["conn"].listingType
- key = curses.KEY_ENTER
+ resolver = panels["conn"].resolver
+ resolver.setPaused(not panels["conn"].allowDNS)
+ for connEntry in panels["conn"].connections: resolver.resolve(connEntry[connPanel.CONN_F_IP])
+ else:
+ panels["control"].resolvingCounter = -1
+ panels["conn"].resolver.setPaused(True)
- # reverts popup dimensions and conn panel label
- panels["popup"].height = 9
- panels["popup"].recreate(stdscr, startY, 80)
- panels["conn"].showLabel = True
-
- # applies new setting
- pickedOption = options[selection]
- if pickedOption != panels["conn"].listingType:
- panels["conn"].listingType = pickedOption
-
- if panels["conn"].listingType == connPanel.LIST_HOSTNAME:
- curses.halfdelay(10) # refreshes display every second until done resolving
- panels["control"].resolvingCounter = panels["conn"].resolver.totalResolves - panels["conn"].resolver.unresolvedQueue.qsize()
-
- resolver = panels["conn"].resolver
- resolver.setPaused(not panels["conn"].allowDNS)
- for connEntry in panels["conn"].connections: resolver.resolve(connEntry[connPanel.CONN_F_IP])
- else:
- panels["control"].resolvingCounter = -1
- panels["conn"].resolver.setPaused(True)
-
- panels["conn"].sortConnections()
-
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- cursesLock.release()
+ panels["conn"].sortConnections()
elif page == 1 and panels["conn"].isCursorEnabled and key in (curses.KEY_ENTER, 10, ord(' ')):
# provides details on selected connection
cursesLock.acquire()
try:
- for key in PAUSEABLE: panels[key].setPaused(True)
+ setPauseState(panels, isPaused, page, True)
popup = panels["popup"]
# reconfigures connection panel to accomidate details dialog
@@ -468,9 +516,10 @@
resolver.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(' ')):
- key = 0
- curses.cbreak() # wait indefinitely for key presses (no timeout)
popup.clear()
popup.win.box()
popup.addstr(0, 0, "Connection Details:", util.LABEL_ATTR)
@@ -577,6 +626,7 @@
panels["conn"].showLabel = True
panels["conn"].showingDetails = False
resolver.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:
cursesLock.release()
@@ -585,7 +635,7 @@
# set ordering for connection listing
cursesLock.acquire()
try:
- for key in PAUSEABLE: panels[key].setPaused(True)
+ setPauseState(panels, isPaused, page, True)
curses.cbreak() # wait indefinitely for key presses (no timeout)
# lists event types
@@ -644,6 +694,7 @@
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:
cursesLock.release()
Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/headerPanel.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -3,7 +3,6 @@
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
import os
-import curses
import socket
from TorCtl import TorCtl
@@ -62,7 +61,6 @@
# Line 2 (authentication label red if open, green if credentials required)
dirPortLabel = "Dir Port: %s, " % self.vals["DirPort"] if self.vals["DirPort"] != "0" else ""
- # TODO: if both cookie and password are set then which takes priority?
if self.vals["IsPasswordAuthSet"]: controlPortAuthLabel = "password"
elif self.vals["IsCookieAuthSet"]: controlPortAuthLabel = "cookie"
else: controlPortAuthLabel = "open"
Modified: arm/trunk/interface/hostnameResolver.py
===================================================================
--- arm/trunk/interface/hostnameResolver.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/hostnameResolver.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -13,11 +13,11 @@
RESOLVER_CACHE_TRIM_SIZE = 2000 # entries removed when max cache size reached
DNS_ERROR_CODES = ("1(FORMERR)", "2(SERVFAIL)", "3(NXDOMAIN)", "4(NOTIMP)", "5(REFUSED)", "6(YXDOMAIN)", "7(YXRRSET)", "8(NXRRSET)", "9(NOTAUTH)", "10(NOTZONE)", "16(BADVERS)")
-class HostnameResolver(Thread):
+class HostnameResolver():
"""
- Background thread that quietly performs reverse DNS lookup of address with
- caching. This is non-blocking, providing None in the case of errors or
- new requests.
+ Provides background threads that quietly performs reverse DNS lookup of
+ address with caching. This is non-blocking, providing None in the case of
+ errors or new requests.
"""
# Resolutions are made using os 'host' calls as opposed to 'gethostbyaddr' in
@@ -30,7 +30,6 @@
# however, I didn't find this to be the case. As always, suggestions welcome!
def __init__(self):
- Thread.__init__(self)
self.resolvedCache = {} # IP Address => (hostname, age) (None if couldn't be resolved)
self.unresolvedQueue = Queue.Queue()
self.recentQueries = [] # recent resolution requests to prevent duplicate requests
Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/logPanel.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -3,7 +3,6 @@
# 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
@@ -153,7 +152,7 @@
Notes event and redraws log. If paused it's held in a temporary buffer.
"""
- self.lastHeartbeat = time.time()
+ if not type.startswith("ARM"): self.lastHeartbeat = time.time()
# strips control characters to avoid screwing up the terminal
msg = "".join([char for char in msg if isprint(char)])
@@ -209,7 +208,7 @@
self.addstr(lineCount, 0, line, util.getColor(color))
lineCount += 1
else:
- (line1, line2) = self._splitLine(line, self.maxX)
+ (line1, line2) = splitLine(line, self.maxX)
self.addstr(lineCount, 0, line1, util.getColor(color))
self.addstr(lineCount + 1, 0, line2, util.getColor(color))
lineCount += 2
@@ -240,27 +239,29 @@
"""
return time.time() - self.lastHeartbeat
+
+def splitLine(message, x):
+ """
+ Divides message into two lines, attempting to do it on a wordbreak.
+ """
- # divides long message to cover two lines
- def _splitLine(self, 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()
+ 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(" ")
- # 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)
+ # 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)
Modified: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/interface/util.py 2009-07-30 07:09:47 UTC (rev 20198)
@@ -83,6 +83,7 @@
self.startY = -1 # top in parent window when created
self.height = height # preferred (max) height of panel, -1 if infinite
self.isDisplaced = False # window isn't in the right location - don't redraw
+ self.maxY, self.maxX = -1, -1
self._resetBounds() # sets last known dimensions of win (maxX and maxY)
def redraw(self):
@@ -114,7 +115,7 @@
newHeight = max(0, y - startY)
if self.height != -1: newHeight = min(newHeight, self.height)
- if self.startY != startY or newHeight > self.maxY or self.isDisplaced or (self.maxX > maxX and maxX != -1):
+ if self.startY != startY or newHeight > self.maxY or self.isDisplaced or (self.maxX != maxX and maxX != -1):
# window growing or moving - recreate
self.startY = startY
startY = min(startY, y - 1) # better create a displaced window than leave it as None
Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt 2009-07-29 19:08:18 UTC (rev 20197)
+++ arm/trunk/readme.txt 2009-07-30 07:09:47 UTC (rev 20198)
@@ -1,6 +1,7 @@
-arm (arm relay monitor) - Terminal status monitor for Tor relays.
+arm (anonymizing relay monitor) - Terminal status monitor for Tor relays.
Developed by Damian Johnson (www.atagar.com - atagar1@xxxxxxxxx)
All code under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+Project page: www.atagar.com/arm
Description:
Command line application for monitoring Tor relays, providing real time status information such as the current configuration, bandwidth usage, message log, current connections, etc. This uses a curses interface much like 'top' does for system usage.
Added: arm/trunk/screenshot_page1.png
===================================================================
(Binary files differ)
Property changes on: arm/trunk/screenshot_page1.png
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream
Added: arm/trunk/screenshot_page2.png
===================================================================
(Binary files differ)
Property changes on: arm/trunk/screenshot_page2.png
___________________________________________________________________
Added: svn:mime-type
+ application/octet-stream