[or-cvs] r20493: {arm} Several substantial features (last tasks for arm's todo list (in arm/trunk: . interface)
atagar at seul.org
atagar at seul.org
Mon Sep 7 05:21:43 UTC 2009
Author: atagar
Date: 2009-09-07 01:21:42 -0400 (Mon, 07 Sep 2009)
New Revision: 20493
Added:
arm/trunk/interface/cpuMemMonitor.py
Modified:
arm/trunk/interface/connCountMonitor.py
arm/trunk/interface/connPanel.py
arm/trunk/interface/controller.py
arm/trunk/interface/headerPanel.py
arm/trunk/interface/logPanel.py
arm/trunk/interface/util.py
arm/trunk/readme.txt
Log:
Several substantial features (last tasks for arm's todo list).
added: scroll bars for connections listing and event log
added: made log scrollable (feature request by StrangeCharm)
added: regular expression filtering for log (feature request by StrangeCharm)
added: connection uptimes (time since connection was first made)
added: identifying client from server connections and providing popup for client circuits
added: graph for system resource usage (cpu/memory)
change: removed cursor toggling option for connection page
fix: minor display issue when changing event types
Modified: arm/trunk/interface/connCountMonitor.py
===================================================================
--- arm/trunk/interface/connCountMonitor.py 2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/connCountMonitor.py 2009-09-07 05:21:42 UTC (rev 20493)
@@ -12,13 +12,14 @@
class ConnCountMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
"""
Tracks number of connections, using cached values in connPanel if recent
- enough (otherwise retrieved independently).
+ enough (otherwise retrieved independently). Client connections are counted
+ as outbound.
"""
def __init__(self, connectionPanel):
graphPanel.GraphStats.__init__(self)
TorCtl.PostEventListener.__init__(self)
- graphPanel.GraphStats.initialize(self, connPanel.TYPE_COLORS["inbound"], connPanel.TYPE_COLORS["outbound"], 10)
+ graphPanel.GraphStats.initialize(self, "green", "cyan", 10)
self.connectionPanel = connectionPanel # connection panel, used to limit netstat calls
def bandwidth_event(self, event):
@@ -28,7 +29,7 @@
if self.connectionPanel.lastUpdate + 1 >= time.time():
# reuses netstat results if recent enough
counts = self.connectionPanel.connectionCount
- self._processEvent(counts[0], counts[1])
+ self._processEvent(counts[0], counts[1] + counts[2])
else:
# cached results stale - requery netstat
inbound, outbound, control = 0, 0, 0
Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py 2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/connPanel.py 2009-09-07 05:21:42 UTC (rev 20493)
@@ -17,15 +17,15 @@
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", "localhost": "yellow"}
-TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "control": 2, "localhost": 3} # defines ordering
+TYPE_COLORS = {"inbound": "green", "outbound": "blue", "client": "cyan", "control": "red", "localhost": "yellow"}
+TYPE_WEIGHTS = {"inbound": 0, "outbound": 1, "client": 2, "control": 3, "localhost": 4} # 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)
+CONN_TYPE, CONN_L_IP, CONN_L_PORT, CONN_F_IP, CONN_F_PORT, CONN_COUNTRY, CONN_TIME = range(7)
# 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)
+ORD_TYPE, ORD_FOREIGN_LISTING, ORD_SRC_LISTING, ORD_DST_LISTING, ORD_COUNTRY, ORD_FOREIGN_PORT, ORD_SRC_PORT, ORD_DST_PORT, ORD_TIME = range(9)
SORT_TYPES = [(ORD_TYPE, "Connection Type",
lambda x, y: TYPE_WEIGHTS[x[CONN_TYPE]] - TYPE_WEIGHTS[y[CONN_TYPE]]),
(ORD_FOREIGN_LISTING, "Listing (Foreign)", None),
@@ -38,7 +38,9 @@
(ORD_SRC_PORT, "Port (Source)",
lambda x, y: int(x[CONN_F_PORT] if x[CONN_TYPE] == "inbound" else x[CONN_L_PORT]) - int(y[CONN_F_PORT] if y[CONN_TYPE] == "inbound" else y[CONN_L_PORT])),
(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]))]
+ 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])),
+ (ORD_TIME, "Connection Time",
+ lambda x, y: cmp(-x[CONN_TIME], -y[CONN_TIME]))]
# provides bi-directional mapping of sorts with their associated labels
def getSortLabel(sortType, withColor = False):
@@ -62,6 +64,7 @@
elif label.startswith("Port"): color = "green"
elif label == "Bandwidth": color = "cyan"
elif label == "Country Code": color = "yellow"
+ elif label == "Connection Time": color = "magenta"
if color: return "<%s>%s</%s>" % (color, label, color)
else: return label
@@ -106,6 +109,8 @@
self.providedGeoipWarning = False
self.orconnStatusCache = [] # cache for 'orconn-status' calls
self.orconnStatusCacheValid = False # indicates if cache has been invalidated
+ self.clientConnectionCache = None # listing of nicknames for our client connections
+ self.clientConnectionLock = RLock() # lock for clientConnectionCache
self.isCursorEnabled = True
self.cursorSelection = None
@@ -121,11 +126,17 @@
self.connections = []
self.connectionsLock = RLock() # limits modifications of connections
- # count of total inbound, outbound, and control connections
- self.connectionCount = [0, 0, 0]
+ # count of total inbound, outbound, client, and control connections
+ self.connectionCount = [0, 0, 0, 0]
self.reset()
+ # change in client circuits
+ def circ_status_event(self, event):
+ self.clientConnectionLock.acquire()
+ self.clientConnectionCache = None
+ self.clientConnectionLock.release()
+
# when consensus changes update fingerprint mappings
def new_consensus_event(self, event):
self.orconnStatusCacheValid = False
@@ -147,7 +158,9 @@
if k in self.nicknameLookupCache.keys(): del self.nicknameLookupCache[k]
# gets consensus data for the new description
- nsData = self.conn.get_network_status("id/%s" % fingerprint)
+ try: nsData = self.conn.get_network_status("id/%s" % fingerprint)
+ except TorCtl.ErrorReply: return
+
if len(nsData) > 1:
# multiple records for fingerprint (shouldn't happen)
self.logger.monitor_event("WARN", "Multiple consensus entries for fingerprint: %s" % fingerprint)
@@ -178,9 +191,18 @@
if self.isPaused or not self.pid: return
self.connectionsLock.acquire()
+ self.clientConnectionLock.acquire()
try:
+ if self.clientConnectionCache == None:
+ # client connection cache was invalidated
+ self.clientConnectionCache = _getClientConnections(self.conn)
+
+ connTimes = {} # mapping of ip/port to connection time
+ for entry in self.connections:
+ connTimes[(entry[CONN_F_IP], entry[CONN_F_PORT])] = entry[CONN_TIME]
+
self.connections = []
- self.connectionCount = [0, 0, 0]
+ self.connectionCount = [0, 0, 0, 0]
# looks at netstat for tor with stderr redirected to /dev/null, options are:
# n = prevents dns lookups, p = include process (say if it's tor), t = tcp only
@@ -200,10 +222,23 @@
self.connectionCount[0] += 1
elif localPort == self.controlPort:
type = "control"
- self.connectionCount[2] += 1
+ self.connectionCount[3] += 1
else:
- type = "outbound"
- self.connectionCount[1] += 1
+ fingerprint = self.getFingerprint(foreignIP, foreignPort)
+ nickname = self.getNickname(foreignIP, foreignPort)
+
+ isClient = False
+ for clientName in self.clientConnectionCache:
+ if nickname == clientName or (len(clientName) > 1 and clientName[0] == "$" and fingerprint == clientName[1:]):
+ isClient = True
+ break
+
+ if isClient:
+ type = "client"
+ self.connectionCount[2] += 1
+ else:
+ type = "outbound"
+ self.connectionCount[1] += 1
try:
countryCodeQuery = "ip-to-country/%s" % foreign[:foreign.find(":")]
@@ -214,7 +249,10 @@
self.logger.monitor_event("WARN", "Tor geoip database is unavailable.")
self.providedGeoipWarning = True
- self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode))
+ if (foreignIP, foreignPort) in connTimes: connTime = connTimes[(foreignIP, foreignPort)]
+ else: connTime = time.time()
+
+ self.connections.append((type, localIP, localPort, foreignIP, foreignPort, countryCode, connTime))
except IOError:
# netstat call failed
self.logger.monitor_event("WARN", "Unable to query netstat for new connections")
@@ -236,7 +274,10 @@
except socket.error:
selfCountryCode = "??"
- self.localhostEntry = (("localhost", selfAddress, selfPort, selfAddress, selfPort, selfCountryCode), selfFingerprint)
+ if (selfAddress, selfPort) in connTimes: connTime = connTimes[(selfAddress, selfPort)]
+ else: connTime = time.time()
+
+ self.localhostEntry = (("localhost", selfAddress, selfPort, selfAddress, selfPort, selfCountryCode, connTime), selfFingerprint)
self.connections.append(self.localhostEntry[0])
else:
self.localhostEntry = None
@@ -248,6 +289,7 @@
if self.listingType != LIST_HOSTNAME: self.sortConnections()
finally:
self.connectionsLock.release()
+ self.clientConnectionLock.release()
def handleKey(self, key):
# cursor or scroll movement
@@ -281,8 +323,6 @@
else: self.scroll = newLoc
finally:
self.connectionsLock.release()
- elif key == ord('c') or key == ord('C'):
- self.isCursorEnabled = not self.isCursorEnabled
elif key == ord('r') or key == ord('R'):
self.allowDNS = not self.allowDNS
if not self.allowDNS: self.resolver.setPaused(True)
@@ -299,12 +339,20 @@
if self.listingType == LIST_HOSTNAME: self.sortConnections()
self.clear()
- if self.showLabel: self.addstr(0, 0, "Connections (%i inbound, %i outbound, %i control):" % tuple(self.connectionCount), util.LABEL_ATTR)
+ clientCountLabel = "" if self.connectionCount[2] == 0 else "%i client, " % self.connectionCount[2]
+ if self.showLabel: self.addstr(0, 0, "Connections (%i inbound, %i outbound, %s%i control):" % (self.connectionCount[0], self.connectionCount[1], clientCountLabel, self.connectionCount[3]), util.LABEL_ATTR)
if self.connections:
listingHeight = self.maxY - 1
- if self.showingDetails: listingHeight -= 8
+ currentTime = time.time()
+ if self.showingDetails:
+ listingHeight -= 8
+ isScrollBarVisible = len(self.connections) > self.maxY - 9
+ else:
+ isScrollBarVisible = len(self.connections) > self.maxY - 1
+ xOffset = 3 if isScrollBarVisible else 0 # content offset for scroll bar
+
# ensure cursor location and scroll top are within bounds
self.cursorLoc = max(min(self.cursorLoc, len(self.connections) - 1), 0)
self.scroll = max(min(self.scroll, len(self.connections) - listingHeight), 0)
@@ -345,19 +393,24 @@
else: dst = self.getFingerprint(entry[CONN_F_IP], entry[CONN_F_PORT])
dst = "%-41s" % dst
else:
- src = "%-11s" % self.nickname
+ src = "%-26s" % self.nickname
if entry[CONN_TYPE] == "control": dst = self.nickname
else: dst = self.getNickname(entry[CONN_F_IP], entry[CONN_F_PORT])
- dst = "%-41s" % dst
+ dst = "%-26s" % dst
if type == "inbound": src, dst = dst, src
- lineEntry = "<%s>%s --> %s (<b>%s</b>)</%s>" % (color, src, dst, type.upper(), color)
+ lineEntry = "<%s>%s --> %s %5s (<b>%s</b>)</%s>" % (color, src, dst, util.getTimeLabel(currentTime - entry[CONN_TIME], 1), type.upper(), color)
if self.isCursorEnabled and entry == self.cursorSelection:
lineEntry = "<h>%s</h>" % lineEntry
- offset = 0 if not self.showingDetails else 8
- self.addfstr(lineNum + offset, 0, lineEntry)
+ yOffset = 0 if not self.showingDetails else 8
+ self.addfstr(lineNum + yOffset, xOffset, lineEntry)
lineNum += 1
+
+ if isScrollBarVisible:
+ topY = 9 if self.showingDetails else 1
+ bottomEntry = self.scroll + self.maxY - 9 if self.showingDetails else self.scroll + self.maxY - 1
+ util.drawScrollBar(self, topY, self.maxY - 1, self.scroll, bottomEntry, len(self.connections))
self.refresh()
finally:
@@ -382,7 +435,7 @@
match = None
# orconn-status provides a listing of Tor's current connections - used to
- # eliminated ambiguity for inbound connections
+ # eliminated ambiguity for outbound connections
if not self.orconnStatusCacheValid:
self.orconnStatusCache, isOdd = [], True
self.orconnStatusCacheValid = True
@@ -521,6 +574,7 @@
if not nsList:
try: nsList = conn.get_network_status()
except TorCtl.TorCtlClosed: nsList = []
+ except TorCtl.ErrorReply: nsList = []
for entry in nsList:
if entry.ip in ipToFingerprint.keys(): ipToFingerprint[entry.ip].append((entry.orport, entry.idhex, entry.nickname))
@@ -528,3 +582,18 @@
return ipToFingerprint
+# provides client relays we're currently attached to (first hops in circuits)
+# this consists of the nicknames and ${fingerprint} if unnamed
+def _getClientConnections(conn):
+ clients = []
+
+ try:
+ for line in conn.get_info("circuit-status")["circuit-status"].split("\n"):
+ components = line.split()
+ if len(components) > 3: clients += [components[2].split(",")[0]]
+ except TorCtl.ErrorReply: pass
+ except TorCtl.TorCtlClosed: pass
+ except socket.error: pass
+
+ return clients
+
Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py 2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/controller.py 2009-09-07 05:21:42 UTC (rev 20493)
@@ -6,6 +6,7 @@
Curses (terminal) interface for the arm relay status monitor.
"""
+import re
import os
import time
import curses
@@ -21,11 +22,13 @@
import util
import bandwidthMonitor
+import cpuMemMonitor
import connCountMonitor
REFRESH_RATE = 5 # seconds between redrawing screen
cursesLock = RLock() # global curses lock (curses isn't thread safe and
# concurrency bugs produce especially sinister glitches)
+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)
@@ -39,7 +42,7 @@
PAUSEABLE = ["header", "graph", "log", "conn"]
# events needed for panels other than the event log
-REQ_EVENTS = ["BW", "NEWDESC", "NEWCONSENSUS"]
+REQ_EVENTS = ["BW", "NEWDESC", "NEWCONSENSUS", "CIRC"]
class ControlPanel(util.Panel):
""" Draws single line label for interface controls. """
@@ -265,6 +268,7 @@
# statistical monitors for graph
panels["graph"].addStats("bandwidth", bandwidthMonitor.BandwidthMonitor(conn))
+ panels["graph"].addStats("cpu / memory", cpuMemMonitor.CpuMemMonitor(panels["header"]))
panels["graph"].addStats("connection count", connCountMonitor.ConnCountMonitor(panels["conn"]))
panels["graph"].setStats("bandwidth")
@@ -272,6 +276,7 @@
sighupTracker = sighupListener()
conn.add_event_listener(panels["log"])
conn.add_event_listener(panels["graph"].stats["bandwidth"])
+ conn.add_event_listener(panels["graph"].stats["cpu / memory"])
conn.add_event_listener(panels["graph"].stats["connection count"])
conn.add_event_listener(panels["conn"])
conn.add_event_listener(sighupTracker)
@@ -284,6 +289,7 @@
isPaused = False # if true updates are frozen
page = 0
netstatRefresh = time.time() # time of last netstat refresh
+ regexFilters = [] # previously used log regex filters
while True:
# tried only refreshing when the screen was resized but it caused a
@@ -329,7 +335,7 @@
# if it's been at least five seconds since the last refresh of connection listing, update
currentTime = time.time()
- if not panels["conn"].isPaused and currentTime - netstatRefresh >= 5:
+ if not panels["conn"].isPaused and (currentTime - netstatRefresh >= 5):
panels["conn"].reset()
netstatRefresh = currentTime
@@ -392,6 +398,9 @@
popup.addfstr(1, 41, "i: graph update interval (<b>%s</b>)" % panels["graph"].updateInterval)
popup.addfstr(2, 2, "b: graph bounds (<b>%s</b>)" % graphPanel.BOUND_LABELS[panels["graph"].bounds])
popup.addstr(2, 41, "e: change logged events")
+
+ regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
+ popup.addfstr(3, 2, "r: log regex filter (<b>%s</b>)" % regexLabel)
if page == 1:
popup.addstr(1, 2, "up arrow: scroll up a line")
popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -407,7 +416,8 @@
popup.addfstr(4, 41, "r: permit DNS resolution (<b>%s</b>)" % allowDnsLabel)
popup.addstr(5, 2, "s: sort ordering")
- popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
+ popup.addstr(5, 41, "c: client circuits")
+ #popup.addfstr(5, 41, "c: toggle cursor (<b>%s</b>)" % ("on" if panels["conn"].isCursorEnabled else "off"))
elif page == 2:
popup.addstr(1, 2, "up arrow: scroll up a line")
popup.addstr(1, 41, "down arrow: scroll down a line")
@@ -421,7 +431,6 @@
popup.addfstr(3, 41, "n: line numbering (<b>%s</b>)" % lineNumLabel)
popup.addstr(7, 2, "Press any key...")
-
popup.refresh()
curses.cbreak()
@@ -503,12 +512,16 @@
# lists event types
popup = panels["popup"]
+ popup.height = 10
+ popup.recreate(stdscr, popup.startY, 80)
+
popup.clear()
+ popup.win.box()
popup.addstr(0, 0, "Event Types:", util.LABEL_ATTR)
lineNum = 1
for line in logPanel.EVENT_LISTING.split("\n"):
line = line[6:]
- popup.addstr(lineNum, 0, line)
+ popup.addstr(lineNum, 1, line)
lineNum += 1
popup.refresh()
@@ -533,10 +546,82 @@
panels["control"].redraw()
time.sleep(2)
+ # reverts popup dimensions
+ popup.height = 9
+ popup.recreate(stdscr, popup.startY, 80)
+
panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
setPauseState(panels, isPaused, page)
finally:
cursesLock.release()
+ elif page == 0 and (key == ord('r') or key == ord('R')):
+ # 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()
+ 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
+ cursesLock.acquire()
+ try:
+ # provides prompt
+ panels["control"].setMsg("Regular expression: ")
+ panels["control"].redraw()
+
+ # 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()
+ time.sleep(2)
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+ finally:
+ cursesLock.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
+ panels["log"].monitor_event("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
@@ -797,6 +882,61 @@
curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
finally:
cursesLock.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 TorCtl.ErrorReply: pass
+ except TorCtl.TorCtlClosed: pass
+ except socket.error: pass
+
+ maxEntryLength = 0
+ if clientCircuits:
+ for clientEntry in clientCircuits: maxEntryLength = max(len(clientEntry), maxEntryLength)
+
+ cursesLock.acquire()
+ try:
+ setPauseState(panels, isPaused, page, True)
+
+ # makes sure there's room for the longest entry
+ popup = panels["popup"]
+ popup._resetBounds()
+ if clientCircuits and maxEntryLength + 4 > popup.maxX:
+ popup.height = max(popup.height, len(clientCircuits) + 3)
+ popup.recreate(stdscr, popup.startY, maxEntryLength + 4)
+
+ # lists commands
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Client Circuits:", util.LABEL_ATTR)
+
+ 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, popup.startY, 80)
+
+ setPauseState(panels, isPaused, page)
+ finally:
+ cursesLock.release()
+ elif page == 0:
+ panels["log"].handleKey(key)
elif page == 1:
panels["conn"].handleKey(key)
elif page == 2:
Added: arm/trunk/interface/cpuMemMonitor.py
===================================================================
--- arm/trunk/interface/cpuMemMonitor.py (rev 0)
+++ arm/trunk/interface/cpuMemMonitor.py 2009-09-07 05:21:42 UTC (rev 20493)
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# cpuMemMonitor.py -- Tracks cpu and memory usage of Tor.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import time
+from TorCtl import TorCtl
+
+import util
+import graphPanel
+
+class CpuMemMonitor(graphPanel.GraphStats, TorCtl.PostEventListener):
+ """
+ Tracks system resource usage (cpu and memory usage), using cached values in
+ headerPanel if recent enough (otherwise retrieved independently).
+ """
+
+ def __init__(self, headerPanel):
+ graphPanel.GraphStats.__init__(self)
+ TorCtl.PostEventListener.__init__(self)
+ graphPanel.GraphStats.initialize(self, "green", "cyan", 10)
+ self.headerPanel = headerPanel # header panel, used to limit ps calls
+
+ def bandwidth_event(self, event):
+ # doesn't use events but this keeps it in sync with the bandwidth panel
+ # (and so it stops if Tor stops
+ if self.headerPanel.lastUpdate + 1 >= time.time():
+ # reuses ps results if recent enough
+ self._processEvent(float(self.headerPanel.vals["%cpu"]), float(self.headerPanel.vals["rss"]) / 1024.0)
+ else:
+ # cached results stale - requery ps
+ inbound, outbound, control = 0, 0, 0
+ psCall = os.popen('ps -p %s -o %s' % (self.headerPanel.vals["pid"], "%cpu,rss"))
+ try:
+ sampling = psCall.read().strip().split()[2:]
+ psCall.close()
+
+ if len(sampling) < 2:
+ # either ps failed or returned no tor instance, register error
+ raise IOError()
+ else:
+ self._processEvent(float(sampling[0]), float(sampling[1]) / 1024.0)
+ except IOError:
+ # ps call failed
+ self.connectionPanel.monitor_event("WARN", "Unable to query ps for resource usage")
+
+ def getTitle(self, width):
+ return "System Resources:"
+
+ def getHeaderLabel(self, width, isPrimary):
+ avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+ if isPrimary: return "CPU (%s%%, avg: %0.1f%%):" % (self.lastPrimary, avg)
+ else: return "Memory (%s, avg: %s):" % (util.getSizeLabel(self.lastSecondary * 1048576, 1), util.getSizeLabel(avg * 1048576, 1))
+
Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py 2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/headerPanel.py 2009-09-07 05:21:42 UTC (rev 20493)
@@ -3,6 +3,7 @@
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
import os
+import time
import socket
from TorCtl import TorCtl
@@ -42,6 +43,7 @@
self.conn = conn # Tor control port connection
self.isPaused = False
self.isWide = False # doubles up parameters to shorten section if room's available
+ self.lastUpdate = -1 # time last stats was retrived
self._updateParams()
def recreate(self, stdscr, startY, maxX=-1):
@@ -212,4 +214,6 @@
for i in range(len(psParams)):
self.vals[psParams[i]] = sampling[i]
+
+ self.lastUpdate = time.time()
Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py 2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/logPanel.py 2009-09-07 05:21:42 UTC (rev 20493)
@@ -2,13 +2,15 @@
# logPanel.py -- Resources related to Tor event monitoring.
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+import re
import time
+import curses
from curses.ascii import isprint
from TorCtl import TorCtl
import util
-MAX_LOG_ENTRIES = 80 # size of log buffer (max number of entries)
+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"}
EVENT_TYPES = {
@@ -27,7 +29,7 @@
k NEWCONSENSUS u CLIENTS_SEEN
Aliases: A All Events X No Events U Unknown Events
DINWE Runlevel and higher severity"""
-
+
def expandEvents(eventAbbr):
"""
Expands event abbreviations to their full names. Beside mappings privided in
@@ -73,19 +75,38 @@
def __init__(self, lock, loggedEvents):
TorCtl.PostEventListener.__init__(self)
util.Panel.__init__(self, lock, -1)
+ 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)
+ def handleKey(self, key):
+ # scroll movement
+ if key in (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE):
+ self._resetBounds()
+ pageHeight, shift = self.maxY - 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):
- 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")
+ 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 stream_status_event(self, event):
# TODO: not sure how to stimulate event - needs sanity check
@@ -179,10 +200,14 @@
try:
self.clear()
+ isScrollBarVisible = self.getLogDisplayLength() > self.maxY - 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"
eventsListing = ", ".join(self.loggedEvents)
+ filterLabel = "" if not self.regexFilter else " - filter: %s" % self.regexFilter.pattern
firstLabelLen = eventsListing.find(", ")
if firstLabelLen == -1: firstLabelLen = len(eventsListing)
@@ -190,34 +215,59 @@
if self.maxX > 10 + firstLabelLen:
eventsLabel += " ("
+
if len(eventsListing) > self.maxX - 11:
labelBreak = eventsListing[:self.maxX - 12].rfind(", ")
eventsLabel += "%s..." % eventsListing[:labelBreak]
- else: eventsLabel += eventsListing
+ elif len(eventsListing) + len(filterLabel) > self.maxX - 11:
+ eventsLabel += eventsListing
+ else: eventsLabel += eventsListing + filterLabel
eventsLabel += ")"
eventsLabel += ":"
self.addstr(0, 0, eventsLabel, util.LABEL_ATTR)
# log entries
- lineCount = 1
+ maxLoc = self.getLogDisplayLength() - self.maxY + 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) < self.maxX:
- self.addstr(lineCount, 0, line, util.getColor(color))
+ if lineCount >= 1: self.addstr(lineCount, xOffset, line, util.getColor(color))
lineCount += 1
else:
- (line1, line2) = splitLine(line, self.maxX)
- self.addstr(lineCount, 0, line1, util.getColor(color))
- self.addstr(lineCount + 1, 0, line2, util.getColor(color))
+ (line1, line2) = splitLine(line, self.maxX - xOffset)
+ if lineCount >= 1: self.addstr(lineCount, xOffset, line1, util.getColor(color))
+ if lineCount >= 0: self.addstr(lineCount + 1, xOffset, line2, util.getColor(color))
lineCount += 2
if lineCount >= self.maxY: break # further log messages wouldn't fit
+
+ if isScrollBarVisible: util.drawScrollBar(self, 1, self.maxY - 1, self.scroll, self.scroll + self.maxY - 1, self.getLogDisplayLength())
self.refresh()
finally:
self.lock.release()
+ def getLogDisplayLength(self):
+ """
+ Provides the number of lines the log would currently occupy.
+ """
+
+ logLength = len(self.msgLog)
+
+ # takes into account filtered and wrapped messages
+ self._resetBounds()
+ for (line, color) in self.msgLog:
+ if self.regexFilter and not self.regexFilter.search(line): logLength -= 1
+ elif len(line) >= self.maxX: logLength += 1
+
+ return logLength
+
def setPaused(self, isPause):
"""
If true, prevents message log from being updated with new events.
Modified: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py 2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/interface/util.py 2009-09-07 05:21:42 UTC (rev 20493)
@@ -75,7 +75,7 @@
def getTimeLabel(seconds, decimal = 0):
"""
Concerts seconds into a time label truncated to its most significant units,
- for instance 7500 seconds would return "". Units go up through days.
+ for instance 7500 seconds would return "2h". Units go up through days.
"""
format = "%%.%if" % decimal
@@ -84,6 +84,34 @@
elif seconds >= 60: return (format + "m") % (seconds / 60.0)
else: return "%is" % seconds
+def drawScrollBar(panel, drawTop, drawBottom, top, bottom, size):
+ """
+ Draws scroll bar reflecting position within a vertical listing. This is
+ squared off at the bottom, having a layout like:
+ |
+ *|
+ *|
+ *|
+ |
+ -+
+ """
+
+ barTop = (drawBottom - drawTop) * top / size
+ barSize = (drawBottom - drawTop) * (bottom - top) / size
+
+ # makes sure bar isn't at top or bottom unless really at those extreme bounds
+ if top > 0: barTop = max(barTop, 1)
+ if bottom != size: barTop = min(barTop, drawBottom - drawTop - barSize - 2)
+
+ for i in range(drawBottom - drawTop):
+ if i >= barTop and i <= barTop + barSize:
+ panel.addstr(i + drawTop, 0, " ", curses.A_STANDOUT)
+
+ # draws box around scroll bar
+ panel.win.vline(drawTop, 1, curses.ACS_VLINE, panel.maxY - 2)
+ panel.win.vline(drawBottom, 1, curses.ACS_LRCORNER, 1)
+ panel.win.hline(drawBottom, 0, curses.ACS_HLINE, 1)
+
class Panel():
"""
Wrapper for curses subwindows. This provides safe proxies to common methods
Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt 2009-09-07 03:09:49 UTC (rev 20492)
+++ arm/trunk/readme.txt 2009-09-07 05:21:42 UTC (rev 20493)
@@ -12,9 +12,10 @@
status. Releases should be stable so if you manage to make it crash (or have a
feature request) then please let me know!
-The project was originally proposed in 2008 by Jacob and Karsten
-(http://archives.seul.org/or/dev/Jan-2008/msg00005.html). An interview by
-Brenno Winter discussing the project is available at:
+The project was originally proposed in 2008 by Jacob and Karsten:
+ http://archives.seul.org/or/dev/Jan-2008/msg00005.html
+
+An interview by Brenno Winter discussing the project is available at:
http://www.atagar.com/arm/HFM_INT_0001.mp3
Requirements:
More information about the tor-commits
mailing list