[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r19953: {arm} Preliminary connection page and miscellaneous additions. add (in arm/trunk: . interface)
Author: atagar
Date: 2009-07-08 17:13:28 -0400 (Wed, 08 Jul 2009)
New Revision: 19953
Added:
arm/trunk/interface/connPanel.py
Modified:
arm/trunk/interface/bandwidthPanel.py
arm/trunk/interface/confPanel.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:
Preliminary connection page and miscellaneous additions.
added: basic connection listing page (using netstat results)
added: 'addfstr' to util which allows for embedded formatting tags (VERY helpful)
added: help shows page's current settings
added: made bandwidth panel toggleable
added: avg bandwidth to bottom of panel
bug fix: prevented header from being paused on page change
bug fix: prevented bandwidth accounting events from being lost when paused
Modified: arm/trunk/interface/bandwidthPanel.py
===================================================================
--- arm/trunk/interface/bandwidthPanel.py 2009-07-08 20:40:04 UTC (rev 19952)
+++ arm/trunk/interface/bandwidthPanel.py 2009-07-08 21:13:28 UTC (rev 19953)
@@ -25,8 +25,8 @@
if conn: self.isAccounting = conn.get_info('accounting/enabled')['accounting/enabled'] == '1'
else: self.isAccounting = False
- height = 12 if self.isAccounting else 9
- util.Panel.__init__(self, lock, height)
+ self.contentHeight = 13 if self.isAccounting else 10
+ util.Panel.__init__(self, lock, self.contentHeight)
self.conn = conn # Tor control port connection
self.tick = 0 # number of updates performed
@@ -36,12 +36,17 @@
self.maxUploadRate = 1
self.accountingInfo = None # accounting data (set by _updateAccountingInfo method)
self.isPaused = False
+ self.isVisible = True
self.pauseBuffer = None # mirror instance used to track updates when paused
# graphed download (read) and upload (write) rates - first index accumulator
self.downloadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
self.uploadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
+ # used to calculate averages, uses tick for time
+ self.totalDownload = 0
+ self.totalUpload = 0
+
# retrieves static stats for label
if conn:
bwStats = conn.get_option(['BandwidthRate', 'BandwidthBurst'])
@@ -50,7 +55,7 @@
else: self.bwRate, self.bwBurst = -1, -1
def bandwidth_event(self, event):
- if self.isPaused: self.pauseBuffer.bandwidth_event(event)
+ if self.isPaused or not self.isVisible: self.pauseBuffer.bandwidth_event(event)
else:
self.lastDownloadRate = event.read
self.lastUploadRate = event.written
@@ -58,6 +63,9 @@
self.downloadRates[0] += event.read
self.uploadRates[0] += event.written
+ self.totalDownload += event.read
+ self.totalUpload += event.written
+
self.tick += 1
if self.tick % BANDWIDTH_GRAPH_SAMPLES == 0:
self.maxDownloadRate = max(self.maxDownloadRate, self.downloadRates[0])
@@ -112,8 +120,17 @@
for row in range(colHeight):
self.addstr(7 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
+ # provides average dl/ul rates
+ if self.tick > 0:
+ avgDownload = self.totalDownload / self.tick
+ avgUpload = self.totalUpload / self.tick
+ else: avgDownload, avgUpload = 0, 0
+ self.addstr(8, 1, "avg: %s/sec" % util.getSizeLabel(avgDownload), dlColor)
+ self.addstr(8, 36, "avg: %s/sec" % util.getSizeLabel(avgUpload), ulColor)
+
+ # accounting stats if enabled
if self.isAccounting:
- if not self.isPaused: self._updateAccountingInfo()
+ if not self.isPaused and self.isVisible: self._updateAccountingInfo()
if self.accountingInfo:
status = self.accountingInfo["status"]
@@ -121,16 +138,12 @@
if status == "soft": hibernateColor = "yellow"
elif status == "hard": hibernateColor = "red"
- self.addstr(9, 0, "Accounting (", curses.A_BOLD)
- self.addstr(9, 12, status, curses.A_BOLD | util.getColor(hibernateColor))
- self.addstr(9, 12 + len(status), "):", curses.A_BOLD)
-
- self.addstr(9, 35, "Time to reset: %s" % self.accountingInfo["resetTime"])
- self.addstr(10, 2, "%s / %s" % (self.accountingInfo["read"], self.accountingInfo["readLimit"]), dlColor)
- self.addstr(10, 37, "%s / %s" % (self.accountingInfo["written"], self.accountingInfo["writtenLimit"]), ulColor)
+ self.addfstr(10, 0, "<b>Accounting (<%s>%s</%s>)" % (hibernateColor, status, hibernateColor))
+ self.addstr(10, 35, "Time to reset: %s" % self.accountingInfo["resetTime"])
+ self.addstr(11, 2, "%s / %s" % (self.accountingInfo["read"], self.accountingInfo["readLimit"]), dlColor)
+ self.addstr(11, 37, "%s / %s" % (self.accountingInfo["written"], self.accountingInfo["writtenLimit"]), ulColor)
else:
- self.addstr(9, 0, "Accounting:", curses.A_BOLD)
- self.addstr(9, 12, "Shutting Down...")
+ self.addfstr(10, 0, "<b>Accounting:</b> Shutting Down...")
self.refresh()
finally:
@@ -142,9 +155,24 @@
"""
if isPause == self.isPaused: return
+ self.isPaused = isPause
+ if self.isVisible: self._parameterSwap()
+
+ def setVisible(self, isVisible):
+ """
+ Toggles panel visability, hiding if false.
+ """
- self.isPaused = isPause
- if self.isPaused:
+ if isVisible == self.isVisible: return
+ self.isVisible = isVisible
+
+ if self.isVisible: self.height = self.contentHeight
+ else: self.height = 0
+
+ if not self.isPaused: self._parameterSwap()
+
+ def _parameterSwap(self):
+ if self.isPaused or not self.isVisible:
if self.pauseBuffer == None: self.pauseBuffer = BandwidthMonitor(None, None)
self.pauseBuffer.tick = self.tick
@@ -154,6 +182,10 @@
self.pauseBuffer.maxUploadRate = self.maxUploadRate
self.pauseBuffer.downloadRates = list(self.downloadRates)
self.pauseBuffer.uploadRates = list(self.uploadRates)
+ self.pauseBuffer.totalDownload = self.totalDownload
+ self.pauseBuffer.totalUpload = self.totalUpload
+ self.pauseBuffer.bwRate = self.bwRate
+ self.pauseBuffer.bwBurst = self.bwBurst
else:
self.tick = self.pauseBuffer.tick
self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
@@ -162,6 +194,10 @@
self.maxUploadRate = self.pauseBuffer.maxUploadRate
self.downloadRates = self.pauseBuffer.downloadRates
self.uploadRates = self.pauseBuffer.uploadRates
+ self.totalDownload = self.pauseBuffer.totalDownload
+ self.totalUpload = self.pauseBuffer.totalUpload
+ self.bwRate = self.pauseBuffer.bwRate
+ self.bwBurst = self.pauseBuffer.bwBurst
self.redraw()
def _updateAccountingInfo(self):
Modified: arm/trunk/interface/confPanel.py
===================================================================
--- arm/trunk/interface/confPanel.py 2009-07-08 20:40:04 UTC (rev 19952)
+++ arm/trunk/interface/confPanel.py 2009-07-08 21:13:28 UTC (rev 19953)
@@ -24,18 +24,21 @@
"""
Reloads torrc contents and resets scroll height.
"""
- confFile = open(self.confLocation, "r")
- self.confContents = confFile.readlines()
- confFile.close()
+ try:
+ confFile = open(self.confLocation, "r")
+ self.confContents = confFile.readlines()
+ confFile.close()
+ except IOError:
+ self.confContents = ["### Unable to load torrc ###"]
self.scroll = 0
def handleKey(self, key):
self._resetBounds()
pageHeight = self.maxY - 1
if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
- elif key == curses.KEY_DOWN: self.scroll = min(self.scroll + 1, len(self.confContents) - pageHeight)
+ 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 = min(self.scroll + pageHeight, len(self.confContents) - pageHeight)
+ 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('r') or key == ord('R'): self.reset()
elif key == ord('s') or key == ord('S'):
Added: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py (rev 0)
+++ arm/trunk/interface/connPanel.py 2009-07-08 21:13:28 UTC (rev 19953)
@@ -0,0 +1,175 @@
+#!/usr/bin/env python
+# connPanel.py -- Lists network connections used by tor.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import curses
+import socket
+from TorCtl import TorCtl
+
+import util
+
+# enums for sorting types
+ORD_TYPE, ORD_FOREIGN_IP, ORD_SRC_IP, ORD_DST_IP, ORD_ALPHANUMERIC, ORD_FOREIGN_PORT, ORD_SRC_PORT, ORD_DST_PORT, ORD_COUNTRY = range(9)
+SORT_TYPES = [(ORD_TYPE, "Connection Type"), (ORD_FOREIGN_IP, "IP (Foreign)"), (ORD_SRC_IP, "IP (Source)"), (ORD_DST_IP, "IP (Dest.)"), (ORD_ALPHANUMERIC, "Alphanumeric"), (ORD_FOREIGN_PORT, "Port (Foreign)"), (ORD_SRC_PORT, "Port (Source)"), (ORD_DST_PORT, "Port (Dest.)"), (ORD_COUNTRY, "Country Code")]
+
+# provides bi-directional mapping of sorts with their associated labels
+def getSortLabel(sortType, withColor = False):
+ """
+ Provides label associated with a type of sorting. Throws ValueEror if no such
+ sort exists. If adding color formatting this wraps with the following mappings:
+ Connection Type red
+ IP * blue
+ Port * green
+ Alphanumeric cyan
+ Country Code yellow
+ """
+
+ for (type, label) in SORT_TYPES:
+ if sortType == type:
+ color = None
+
+ if withColor:
+ if label == "Connection Type": color = "red"
+ elif label.startswith("IP"): color = "blue"
+ elif label.startswith("Port"): color = "green"
+ elif label == "Alphanumeric": color = "cyan"
+ elif label == "Country Code": color = "yellow"
+
+ if color: return "<%s>%s</%s>" % (color, label, color)
+ else: return label
+
+ raise ValueError(sortType)
+
+def getSortType(sortLabel):
+ """
+ Provides sort type associated with a given label. Throws ValueEror if label
+ isn't recognized.
+ """
+
+ for (type, label) in SORT_TYPES:
+ if sortLabel == label: return type
+ raise ValueError(sortLabel)
+
+# TODO: order by bandwidth
+# TODO: primary/secondary sort parameters
+
+class ConnPanel(util.Panel):
+ """
+ Lists netstat provided network data of tor.
+ """
+
+ def __init__(self, lock, conn):
+ util.Panel.__init__(self, lock, -1)
+ self.scroll = 0
+ logger = None
+ self.conn = conn # tor connection for querrying country codes
+ self.logger = logger # notified in case of problems
+ self.sortOrdering = [ORD_TYPE, ORD_SRC_IP, ORD_SRC_PORT]
+
+ # gets process id to make sure we get the correct netstat data
+ psCall = os.popen('ps -C tor -o pid')
+ try: self.pid = psCall.read().strip().split()[1]
+ except IOError:
+ self.logger.monitor_event("ERR", "Unable to resolve tor pid, abandoning connection listing")
+ self.pid = -1 # ps call failed
+ psCall.close()
+
+ self.orPort = self.conn.get_option("ORPort")[0][1]
+ self.dirPort = self.conn.get_option("DirPort")[0][1]
+ self.controlPort = self.conn.get_option("ControlPort")[0][1]
+
+ # tuples of last netstat results with (source, destination)
+ # addresses could be resolved and foreign locations followed by country code
+ self.inboundConn = []
+ self.outboundConn = []
+ self.controlConn = []
+
+ # alternative conn: (source IP, source port destination IP, destination port, country code, type)
+ self.connections = []
+
+ # cache of DNS lookups, IP Address => hostname (None if couldn't be resolved)
+ self.hostnameResolution = {}
+
+ self.reset()
+
+ def reset(self):
+ """
+ Reloads netstat results.
+ """
+
+ self.inboundConn = []
+ self.outboundConn = []
+ self.controlConn = []
+
+ self.connections = []
+
+ # TODO: provide special message if there's no connections
+ if self.pid == -1: return # TODO: how should this be handled?
+
+ # 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
+ netstatCall = os.popen("netstat -npt 2> /dev/null | grep %s/tor" % self.pid)
+ try:
+ results = netstatCall.readlines()
+
+ for line in results:
+ if not line.startswith("tcp"): continue
+ param = line.split()
+ local = param[3]
+ foreign = param[4]
+
+ sourcePort = local[local.find(":") + 1:]
+ if sourcePort == self.controlPort: self.controlConn.append((local, foreign))
+ else:
+ # include country code for foreign address
+ try:
+ countryCodeCommand = "ip-to-country/%s" % foreign[:foreign.find(":")]
+ countryCode = self.conn.get_info(countryCodeCommand)[countryCodeCommand]
+ foreign = "%s (%s)" % (foreign, countryCode)
+ except socket.error: pass
+
+ if sourcePort == self.orPort or sourcePort == self.dirPort: self.inboundConn.append((foreign, local))
+ else: self.outboundConn.append((local, foreign))
+ except IOError:
+ # TODO: provide warning of failure
+ pass # netstat call failed
+ netstatCall.close()
+
+ # sort by local ip address
+ # TODO: implement
+
+ def handleKey(self, key):
+ self._resetBounds()
+ pageHeight = self.maxY - 1
+ if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
+ elif key == curses.KEY_DOWN: self.scroll = max(0, self.scroll + 1)
+ elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - pageHeight, 0)
+ elif key == curses.KEY_NPAGE: self.scroll = max(0, self.scroll + pageHeight)
+ self.redraw()
+
+ def redraw(self):
+ if self.win:
+ if not self.lock.acquire(False): return
+ try:
+ self.clear()
+ self.addstr(0, 0, "Connections (%i inbound, %i outbound, %i control):" % (len(self.inboundConn), len(self.outboundConn), len(self.controlConn)), util.LABEL_ATTR)
+
+ self.scroll = min(self.scroll, len(self.inboundConn) + len(self.outboundConn) + len(self.controlConn) - self.maxY + 1)
+ skipEntries = self.scroll
+ lineNum = 1
+ connSets = [(self.inboundConn, "INBOUND", "green"),
+ (self.outboundConn, "OUTBOUND", "blue"),
+ (self.controlConn, "CONTROL", "red")]
+
+ for connSet in connSets:
+ for (source, dest) in connSet[0]:
+ if skipEntries > 0:
+ skipEntries = skipEntries - 1
+ else:
+ self.addfstr(lineNum, 0, "<%s>%-30s--> %-26s(<b>%s</b>)</%s>" % (connSet[2], source, dest, connSet[1], connSet[2]))
+ lineNum = lineNum + 1
+ self.refresh()
+ finally:
+ self.lock.release()
+
Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py 2009-07-08 20:40:04 UTC (rev 19952)
+++ arm/trunk/interface/controller.py 2009-07-08 21:13:28 UTC (rev 19953)
@@ -13,9 +13,10 @@
import util
import headerPanel
-import confPanel
import bandwidthPanel
import logPanel
+import connPanel
+import confPanel
REFRESH_RATE = 5 # seconds between redrawing screen
cursesLock = RLock() # global curses lock (curses isn't thread safe and
@@ -28,11 +29,11 @@
PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
PAGES = [
["bandwidth", "log"],
+ ["conn"],
["torrc"]]
PAUSEABLE = ["header", "bandwidth", "log"]
-PAGE_COUNT = 2 # all page numbering is internally represented as 0-indexed
-# TODO: page 2: configuration information
-# TODO: page 3: current connections
+PAGE_COUNT = 3 # all page numbering is internally represented as 0-indexed
+# TODO: page for configuration information
class ControlPanel(util.Panel):
""" Draws single line label for interface controls. """
@@ -60,7 +61,7 @@
msgAttr = self.msgAttr
if msgText == CTL_HELP:
- msgText = "page %i / %i - q: quit, p: pause, h: help" % (self.page, PAGE_COUNT)
+ msgText = "page %i / %i - q: quit, p: pause, h: page help" % (self.page, PAGE_COUNT)
msgAttr = curses.A_NORMAL
elif msgText == CTL_PAUSED:
msgText = "Paused"
@@ -127,9 +128,10 @@
panels = {
"header": headerPanel.HeaderPanel(cursesLock, conn),
"control": ControlPanel(cursesLock),
- "popup": util.Panel(cursesLock, 8),
+ "popup": util.Panel(cursesLock, 9),
"bandwidth": bandwidthPanel.BandwidthMonitor(cursesLock, conn),
"log": logPanel.LogMonitor(cursesLock, loggedEvents),
+ "conn": connPanel.ConnPanel(cursesLock, conn),
"torrc": confPanel.ConfPanel(cursesLock, conn.get_info("config-file")["config-file"])}
# listeners that update bandwidth and log panels with Tor status
@@ -144,6 +146,7 @@
isUnresponsive = False # true if it's been over five seconds since the last BW event (probably due to Tor closing)
isPaused = False # if true updates are frozen
page = 0
+ netstatRefresh = time.time() # time of last netstat refresh
while True:
# tried only refreshing when the screen was resized but it caused a
@@ -178,6 +181,12 @@
isUnresponsive = False
panels["log"].monitor_event("WARN", "Relay resumed")
+ # if it's been at least five seconds since the last refresh of connection listing, update
+ currentTime = time.time()
+ if currentTime - netstatRefresh >= 5:
+ panels["conn"].reset()
+ netstatRefresh = currentTime
+
# I haven't the foggiest why, but doesn't work if redrawn out of order...
for panelKey in (PAGE_S + PAGES[page]): panels[panelKey].redraw()
oldY, oldX = y, x
@@ -194,7 +203,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])
+ for key in PAUSEABLE: panels[key].setPaused(isPaused or (key not in PAGES[page] and key not in PAGE_S))
panels["control"].page = page + 1
panels["control"].refresh()
@@ -204,8 +213,7 @@
try:
isPaused = not isPaused
for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
- msgType = CTL_PAUSED if isPaused else CTL_HELP
- panels["control"].setMsg(msgType)
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
finally:
cursesLock.release()
elif key == ord('h') or key == ord('H'):
@@ -214,9 +222,6 @@
try:
for key in PAUSEABLE: panels[key].setPaused(True)
- panels["control"].setMsg("Press any key...")
- panels["control"].redraw()
-
# lists commands
popup = panels["popup"]
popup.clear()
@@ -224,28 +229,48 @@
popup.addstr(0, 0, "Page %i Commands:" % (page + 1), util.LABEL_ATTR)
if page == 0:
- popup.addstr(1, 2, "e: change logged events")
- elif page == 1:
+ bwVisibleLabel = "visible" if panels["bandwidth"].isVisible else "hidden"
+ popup.addfstr(1, 2, "b: toggle <u>b</u>andwidth panel (<b>%s</b>)" % bwVisibleLabel)
+ popup.addfstr(1, 41, "e: change logged <u>e</u>vents")
+ if page == 1:
popup.addstr(1, 2, "up arrow: scroll up a line")
- popup.addstr(1, 35, "down arrow: scroll down a line")
+ popup.addstr(1, 41, "down arrow: scroll down a line")
popup.addstr(2, 2, "page up: scroll up a page")
- popup.addstr(2, 35, "page down: scroll down a page")
- popup.addstr(3, 2, "s: toggle comment stripping")
- popup.addstr(3, 35, "n: toggle line numbering")
- popup.addstr(4, 2, "r: reload torrc")
+ popup.addstr(2, 41, "page down: scroll down a page")
+ #popup.addstr(3, 2, "s: sort ordering")
+ #popup.addstr(4, 2, "r: resolve hostnames")
+ #popup.addstr(4, 41, "R: hostname auto-resolution")
+ #popup.addstr(5, 2, "h: show IP/hostnames")
+ #popup.addstr(5, 41, "c: clear hostname cache")
+ elif page == 2:
+ popup.addstr(1, 2, "up arrow: scroll up a line")
+ popup.addstr(1, 41, "down arrow: scroll down a line")
+ popup.addstr(2, 2, "page up: scroll up a page")
+ popup.addstr(2, 41, "page down: scroll down a page")
+
+ strippingLabel = "on" if panels["torrc"].stripComments else "off"
+ popup.addfstr(3, 2, "s: comment <u>s</u>tripping (<b>%s</b>)" % strippingLabel)
+
+ lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
+ popup.addfstr(3, 41, "n: line <u>n</u>umbering (<b>%s</b>)" % lineNumLabel)
+
+ popup.addfstr(4, 2, "r: <u>r</u>eload torrc")
+ popup.addstr(7, 2, "Press any key...")
+
popup.refresh()
curses.cbreak()
stdscr.getch()
curses.halfdelay(REFRESH_RATE * 10)
- msgType = CTL_PAUSED if isPaused else CTL_HELP
- panels["control"].setMsg(msgType)
-
for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[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('e') or key == ord('E')):
# allow user to enter new types of events to log - unchanged if left blank
cursesLock.acquire()
@@ -267,8 +292,8 @@
popup.addstr(0, 0, "Event Types:", util.LABEL_ATTR)
lineNum = 1
for line in logPanel.EVENT_LISTING.split("\n"):
- line = line.strip()
- popup.addstr(lineNum, 0, line[:x - 1])
+ line = " " + line.strip()
+ popup.addstr(lineNum, 0, line)
lineNum += 1
popup.refresh()
@@ -293,13 +318,78 @@
panels["control"].redraw()
time.sleep(2)
- msgType = CTL_PAUSED if isPaused else CTL_HELP
- panels["control"].setMsg(msgType)
-
+ panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
for key in PAUSEABLE: panels[key].setPaused(isPaused or key not in PAGES[page])
finally:
cursesLock.release()
+ elif page == 1 and (key == ord('s') or key == ord('S')):
+ continue
+
+ # set ordering for connection listing
+ cursesLock.acquire()
+ try:
+ for key in PAUSEABLE: panels[key].setPaused(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) in connPanel.SORT_TYPES: options.append(label)
+ options.append("Cancel")
+
+ while len(selections) < 3:
+ popup.clear()
+ popup.win.box()
+ popup.addstr(0, 0, "Connection Ordering:", util.LABEL_ATTR)
+ 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))
+ options.remove(selection)
+ cursorLoc = min(cursorLoc, len(options) - 1)
+
+ if len(selections) == 3: panels["conn"].sortOrdering = selections
+ curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
+ finally:
+ cursesLock.release()
elif page == 1:
+ panels["conn"].handleKey(key)
+ elif page == 2:
panels["torrc"].handleKey(key)
def startTorMonitor(conn, loggedEvents):
Modified: arm/trunk/interface/headerPanel.py
===================================================================
--- arm/trunk/interface/headerPanel.py 2009-07-08 20:40:04 UTC (rev 19952)
+++ arm/trunk/interface/headerPanel.py 2009-07-08 21:13:28 UTC (rev 19953)
@@ -4,6 +4,7 @@
import os
import curses
+import socket
from TorCtl import TorCtl
import util
@@ -48,7 +49,7 @@
self.addstr(0, 45, "Tor %s" % self.vals["version"])
# Line 2 (authentication label red if open, green if credentials required)
- dirPortLabel = "Dir Port: %s, " % self.vals["DirPort"] if not self.vals["DirPort"] == None else ""
+ 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"
@@ -57,11 +58,7 @@
controlPortAuthColor = "red" if controlPortAuthLabel == "open" else "green"
labelStart = "%s - %s:%s, %sControl Port (" % (self.vals["Nickname"], self.vals["address"], self.vals["ORPort"], dirPortLabel)
- self.addstr(1, 0, labelStart)
- xLoc = len(labelStart)
- self.addstr(1, xLoc, controlPortAuthLabel, util.getColor(controlPortAuthColor))
- xLoc += len(controlPortAuthLabel)
- self.addstr(1, xLoc, "): %s" % self.vals["ControlPort"])
+ self.addfstr(1, 0, "%s<%s>%s</%s>): %s" % (labelStart, controlPortAuthColor, controlPortAuthLabel, controlPortAuthColor, self.vals["ControlPort"]))
# Line 3 (system usage info)
self.addstr(2, 0, "cpu: %s%%" % self.vals["%cpu"])
@@ -108,7 +105,7 @@
if not self.vals:
# retrieves static params
- self.vals = self.conn.get_info(["version", "config-file"])
+ self.vals = self.conn.get_info(["version"])
# populates with some basic system information
unameVals = os.uname()
@@ -133,6 +130,9 @@
except TorCtl.TorCtlClosed:
# Tor shut down - keep last known values
if not self.vals[param]: self.vals[param] = "Unknown"
+ except socket.error:
+ # Can be caused if tor crashed
+ if not self.vals[param]: self.vals[param] = "Unknown"
# ps call provides header followed by params for tor
psParams = ["%cpu", "rss", "%mem", "pid", "etime"]
Modified: arm/trunk/interface/logPanel.py
===================================================================
--- arm/trunk/interface/logPanel.py 2009-07-08 20:40:04 UTC (rev 19952)
+++ arm/trunk/interface/logPanel.py 2009-07-08 21:13:28 UTC (rev 19953)
@@ -101,7 +101,7 @@
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: %i WRITTEN: %i" % (type(event.strm_id), type(event.bytes_read), type(event.bytes_written)), "white")
+ 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()
Modified: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py 2009-07-08 20:40:04 UTC (rev 19952)
+++ arm/trunk/interface/util.py 2009-07-08 21:13:28 UTC (rev 19953)
@@ -3,6 +3,7 @@
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
import curses
+from sys import maxint
LABEL_ATTR = curses.A_STANDOUT # default formatting constant
@@ -16,6 +17,11 @@
("black", curses.COLOR_BLACK),
("white", curses.COLOR_WHITE))
+FORMAT_TAGS = {"<b>": curses.A_BOLD,
+ "<u>": curses.A_UNDERLINE,
+ "<h>": curses.A_STANDOUT}
+for (colorLabel, cursesAttr) in COLOR_LIST: FORMAT_TAGS["<%s>" % colorLabel] = curses.A_NORMAL
+
# foreground color mappings (starts uninitialized - all colors associated with default white fg / black bg)
COLOR_ATTR_INITIALIZED = False
COLOR_ATTR = dict([(color[0], 0) for color in COLOR_LIST])
@@ -38,6 +44,9 @@
colorpair += 1
curses.init_pair(colorpair, fgColor, -1) # -1 allows for default (possibly transparent) background
COLOR_ATTR[name] = curses.color_pair(colorpair)
+
+ # maps color tags to initialized attributes
+ for colorLabel in COLOR_ATTR.keys(): FORMAT_TAGS["<%s>" % colorLabel] = COLOR_ATTR[colorLabel]
def getColor(color):
"""
@@ -140,9 +149,65 @@
# subwindows need a character buffer (either in the x or y direction) from
# actual content to prevent crash when shrank
- if self.win and self.maxX > x and self.maxY > y:
- if not self.isDisplaced: self.win.addstr(y, x, msg[:self.maxX - x - 1], attr)
+ if self.win and self.maxX > x and self.maxY > y and not self.isDisplaced:
+ self.win.addstr(y, x, msg[:self.maxX - x - 1], attr)
+ def addfstr(self, y, x, msg):
+ """
+ Writes string to subwindow. The message can contain xhtml-style tags for
+ formatting, including:
+ <b>text</b> bold
+ <u>text</u> underline
+ <h>text</h> highlight
+ <[color]>text</[color]> use color (see COLOR_LIST for constants)
+
+ Tag nexting is supported and tag closing is not strictly enforced. This
+ does not valididate input and unrecognized tags are treated as normal text.
+ Currently this funtion has the following restrictions:
+ - Duplicate tags nested (such as "<b><b>foo</b></b>") is invalid and may
+ throw an error.
+ - Color tags shouldn't be nested in each other (results are undefined).
+ """
+
+ if self.win and self.maxY > y and not self.isDisplaced:
+ formatting = [curses.A_NORMAL]
+ expectedCloseTags = []
+
+ while self.maxX > x and len(msg) > 0:
+ # finds next consumeable tag
+ nextTag, nextTagIndex = None, maxint
+
+ for tag in FORMAT_TAGS.keys() + expectedCloseTags:
+ tagLoc = msg.find(tag)
+ if tagLoc != -1 and tagLoc < nextTagIndex:
+ nextTag, nextTagIndex = tag, tagLoc
+
+ # splits into text before and after tag
+ if nextTag:
+ msgSegment = msg[:nextTagIndex]
+ msg = msg[nextTagIndex + len(nextTag):]
+ else:
+ msgSegment = msg
+ msg = ""
+
+ # adds text before tag with current formatting
+ attr = 0
+ for format in formatting: attr |= format
+ self.win.addstr(y, x, msgSegment[:self.maxX - x - 1], attr)
+
+ # applies tag attributes for future text
+ if nextTag:
+ if not nextTag.startswith("</"):
+ # open tag - add formatting
+ expectedCloseTags.append("</" + nextTag[1:])
+ formatting.append(FORMAT_TAGS[nextTag])
+ else:
+ # close tag - remove formatting
+ expectedCloseTags.remove(nextTag)
+ formatting.remove(FORMAT_TAGS["<" + nextTag[2:]])
+
+ x += len(msgSegment)
+
def _resetBounds(self):
if self.win: self.maxY, self.maxX = self.win.getmaxyx()
else: self.maxY, self.maxX = -1, -1
Modified: arm/trunk/readme.txt
===================================================================
--- arm/trunk/readme.txt 2009-07-08 20:40:04 UTC (rev 19952)
+++ arm/trunk/readme.txt 2009-07-08 21:13:28 UTC (rev 19953)
@@ -3,13 +3,11 @@
All code under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
Description:
-Command line application for monitoring Tor relays, providing real time status information such as the current configuration, bandwidth usage, message log, etc. This uses a curses interface much like 'top' does for system usage.
+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.
Requirements:
Python 2.5
-TorCtl - This needs to be in your Python path. In Linux this can be done via:
- svn co https://tor-svn.freehaven.net/svn/torctl
- export PYTHONPATH=$PWD/torctl/trunk/python/
+TorCtl (retrieved in svn checkout)
Tor is running with an available control port. This means either...
... starting Tor with '--controlport <PORT>'
... or including 'ControlPort <PORT>' in your torrc