[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r19615: {arm} Introduced wrapper to hide curses ugliness which greatly sim (in arm/trunk: . interface)
Author: atagar
Date: 2009-06-02 04:37:49 -0400 (Tue, 02 Jun 2009)
New Revision: 19615
Added:
arm/trunk/interface/bandwidthPanel.py
arm/trunk/interface/logPanel.py
arm/trunk/interface/staticPanel.py
arm/trunk/interface/util.py
Removed:
arm/trunk/interface/bandwidthMonitor.py
arm/trunk/interface/eventLog.py
Modified:
arm/trunk/arm.py
arm/trunk/interface/controller.py
Log:
Introduced wrapper to hide curses ugliness which greatly simplified interface code. Also fixed all problems with resizing - it's now rock solid.
Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py 2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/arm.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -22,7 +22,7 @@
sys.exit()
from interface import controller
-from interface import eventLog
+from interface import logPanel
DEFAULT_CONTROL_ADDR = "127.0.0.1"
DEFAULT_CONTROL_PORT = 9051
@@ -47,7 +47,7 @@
arm -c authenticate using the default cookie
arm -i 1643 -p prompt for password using control port 1643
arm -e=we -p=nemesis use password 'nemesis' with 'WARN'/'ERR' events
-""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_AUTH_COOKIE, DEFAULT_LOGGED_EVENTS, eventLog.EVENT_LISTING)
+""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_AUTH_COOKIE, DEFAULT_LOGGED_EVENTS, logPanel.EVENT_LISTING)
class Input:
"Collection of the user's command line input"
@@ -174,13 +174,13 @@
# validates and expands logged event flags
try:
- expandedEvents = eventLog.expandEvents(input.loggedEvents)
+ expandedEvents = logPanel.expandEvents(input.loggedEvents)
except ValueError, exc:
for flag in str(exc):
print "Unrecognized event flag: %s" % flag
sys.exit()
- # disables TorCtl from logging events (can possibly interrupt curses)
+ # disables TorCtl from logging events (noisy and can possibly interrupt curses)
TorUtil.loglevel = "NONE"
# attempts to open a socket to the tor server
Deleted: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py 2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/interface/bandwidthMonitor.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -1,143 +0,0 @@
-#!/usr/bin/env python
-# bandwidthMonitor.py -- Resources related to monitoring Tor bandwidth usage.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import curses
-import controller
-from TorCtl import TorCtl
-
-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
-
-def drawBandwidthLabel(screen, staticInfo, maxX):
- """ Draws bandwidth label text (drops stats if not enough room). """
- rateLabel = controller.getSizeLabel(int(staticInfo["BandwidthRate"]))
- burstLabel = controller.getSizeLabel(int(staticInfo["BandwidthBurst"]))
- labelContents = "Bandwidth (cap: %s, burst: %s):" % (rateLabel, burstLabel)
- if maxX < len(labelContents):
- labelContents = "%s):" % labelContents[:labelContents.find(",")] # removes burst measure
- if x < len(labelContents): labelContents = "Bandwidth:" # removes both
-
- screen.erase()
- screen.addstr(0, 0, labelContents[:maxX - 1], controller.LABEL_ATTR)
- screen.refresh()
-
-class BandwidthMonitor(TorCtl.PostEventListener):
- """
- 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.
- """
-
- def __init__(self, bandwidthScreen, cursesLock):
- TorCtl.PostEventListener.__init__(self)
- self.tick = 0 # number of updates performed
- self.bandwidthScreen = bandwidthScreen # curses window where bandwidth's displayed
- 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.isPaused = False
- self.pauseBuffer = None # mirror instance used to track updates when paused
- self.cursesLock = cursesLock # lock to safely use bandwidthScreen
-
- # graphed download (read) and upload (write) rates - first index accumulator
- self.downloadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
- self.uploadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
-
- def bandwidth_event(self, event):
- if self.isPaused:
- self.pauseBuffer.bandwidth_event(event)
- else:
- self.lastDownloadRate = event.read
- self.lastUploadRate = event.written
-
- self.downloadRates[0] += event.read
- self.uploadRates[0] += event.written
-
- 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:]
-
- self.maxUploadRate = max(self.maxUploadRate, self.uploadRates[0])
- self.uploadRates.insert(0, 0)
- del self.uploadRates[BANDWIDTH_GRAPH_COL + 1:]
-
- self.refreshDisplay()
-
- def refreshDisplay(self):
- """ Redraws bandwidth panel. """
-
- # doesn't draw if headless (indicating that the instance is for a pause buffer)
- if self.bandwidthScreen:
- if not self.cursesLock.acquire(False): return
- try:
- self.bandwidthScreen.erase()
- y, x = self.bandwidthScreen.getmaxyx()
- dlColor = controller.COLOR_ATTR[BANDWIDTH_GRAPH_COLOR_DL]
- ulColor = controller.COLOR_ATTR[BANDWIDTH_GRAPH_COLOR_UL]
-
- # current numeric measures
- self.bandwidthScreen.addstr(0, 0, ("Downloaded (%s/sec):" % controller.getSizeLabel(self.lastDownloadRate))[:x - 1], curses.A_BOLD | dlColor)
- if x > 35: self.bandwidthScreen.addstr(0, 35, ("Uploaded (%s/sec):" % controller.getSizeLabel(self.lastUploadRate))[:x - 36], curses.A_BOLD | ulColor)
-
- # graph bounds in KB (uses highest recorded value as max)
- if y > 1:self.bandwidthScreen.addstr(1, 0, ("%4s" % str(self.maxDownloadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES))[:x - 1], dlColor)
- if y > 6: self.bandwidthScreen.addstr(6, 0, " 0"[:x - 1], dlColor)
-
- if x > 35:
- if y > 1: self.bandwidthScreen.addstr(1, 35, ("%4s" % str(self.maxUploadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES))[:x - 36], ulColor)
- if y > 6: self.bandwidthScreen.addstr(6, 35, " 0"[:x - 36], ulColor)
-
- # creates bar graph of bandwidth usage over time
- for col in range(BANDWIDTH_GRAPH_COL):
- if col > x - 8: break
- bytesDownloaded = self.downloadRates[col + 1]
- colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate)
- for row in range(colHeight):
- if y > (6 - row): self.bandwidthScreen.addstr(6 - row, col + 5, " ", curses.A_STANDOUT | dlColor)
-
- for col in range(BANDWIDTH_GRAPH_COL):
- if col > x - 42: break
- bytesUploaded = self.uploadRates[col + 1]
- colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate)
- for row in range(colHeight):
- if y > (6 - row): self.bandwidthScreen.addstr(6 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
-
- self.bandwidthScreen.refresh()
- finally:
- self.cursesLock.release()
-
- def setPaused(self, isPause):
- """
- If true, prevents bandwidth updates from being presented.
- """
-
- if isPause == self.isPaused: return
-
- self.isPaused = isPause
- if self.isPaused:
- if self.pauseBuffer == None:
- self.pauseBuffer = BandwidthMonitor(None, None)
-
- 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)
- else:
- self.tick = self.pauseBuffer.tick
- self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
- self.lastUploadRate = self.pauseBuffer.lastuploadRate
- self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
- self.maxUploadRate = self.pauseBuffer.maxUploadRate
- self.downloadRates = self.pauseBuffer.downloadRates
- self.uploadRates = self.pauseBuffer.uploadRates
- self.refreshDisplay()
-
Copied: arm/trunk/interface/bandwidthPanel.py (from rev 19594, arm/trunk/interface/bandwidthMonitor.py)
===================================================================
--- arm/trunk/interface/bandwidthPanel.py (rev 0)
+++ arm/trunk/interface/bandwidthPanel.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+# bandwidthPanel.py -- Resources related to monitoring Tor bandwidth usage.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+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
+
+def drawBandwidthLabel(scr, staticInfo):
+ """ Draws bandwidth label text (drops stats if not enough room). """
+ scr.clear()
+ maxX = scr.maxX
+
+ rateLabel = util.getSizeLabel(int(staticInfo["BandwidthRate"]))
+ burstLabel = util.getSizeLabel(int(staticInfo["BandwidthBurst"]))
+ labelContents = "Bandwidth (cap: %s, burst: %s):" % (rateLabel, burstLabel)
+ if maxX < len(labelContents):
+ labelContents = "%s):" % labelContents[:labelContents.find(",")] # removes burst measure
+ if maxX < len(labelContents): labelContents = "Bandwidth:" # removes both
+
+ scr.addstr(0, 0, labelContents, util.LABEL_ATTR)
+ scr.refresh()
+
+class BandwidthMonitor(TorCtl.PostEventListener):
+ """
+ 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.
+ """
+
+ def __init__(self, scr):
+ TorCtl.PostEventListener.__init__(self)
+ self.scr = scr # associated subwindow
+ 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.isPaused = False
+ 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)
+
+ def bandwidth_event(self, event):
+ if self.isPaused: self.pauseBuffer.bandwidth_event(event)
+ else:
+ self.lastDownloadRate = event.read
+ self.lastUploadRate = event.written
+
+ self.downloadRates[0] += event.read
+ self.uploadRates[0] += event.written
+
+ 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:]
+
+ self.maxUploadRate = max(self.maxUploadRate, self.uploadRates[0])
+ self.uploadRates.insert(0, 0)
+ del self.uploadRates[BANDWIDTH_GRAPH_COL + 1:]
+
+ self.refreshDisplay()
+
+ def refreshDisplay(self):
+ """ Redraws bandwidth panel. """
+ # doesn't draw if headless (indicating that the instance is for a pause buffer)
+ if self.scr:
+ if not self.scr.lock.acquire(False): return
+ try:
+ self.scr.clear()
+ dlColor = util.getColor(BANDWIDTH_GRAPH_COLOR_DL)
+ ulColor = util.getColor(BANDWIDTH_GRAPH_COLOR_UL)
+
+ # current numeric measures
+ self.scr.addstr(0, 0, "Downloaded (%s/sec):" % util.getSizeLabel(self.lastDownloadRate), curses.A_BOLD | dlColor)
+ self.scr.addstr(0, 35, "Uploaded (%s/sec):" % util.getSizeLabel(self.lastUploadRate), curses.A_BOLD | ulColor)
+
+ # graph bounds in KB (uses highest recorded value as max)
+ self.scr.addstr(1, 0, "%4s" % str(self.maxDownloadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES), dlColor)
+ self.scr.addstr(6, 0, " 0", dlColor)
+
+ self.scr.addstr(1, 35, "%4s" % str(self.maxUploadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES), ulColor)
+ self.scr.addstr(6, 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)
+ for row in range(colHeight):
+ self.scr.addstr(6 - 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)
+ for row in range(colHeight):
+ self.scr.addstr(6 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
+
+ self.scr.refresh()
+ finally:
+ self.scr.lock.release()
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents bandwidth updates from being presented.
+ """
+
+ if isPause == self.isPaused: return
+
+ self.isPaused = isPause
+ if self.isPaused:
+ if self.pauseBuffer == None: self.pauseBuffer = BandwidthMonitor(None)
+
+ 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)
+ else:
+ self.tick = self.pauseBuffer.tick
+ self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
+ self.lastUploadRate = self.pauseBuffer.lastuploadRate
+ self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
+ self.maxUploadRate = self.pauseBuffer.maxUploadRate
+ self.downloadRates = self.pauseBuffer.downloadRates
+ self.uploadRates = self.pauseBuffer.uploadRates
+ self.refreshDisplay()
+
Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py 2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/interface/controller.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
# controller.py -- arm interface (curses monitor for relay status).
# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
@@ -5,156 +6,44 @@
Curses (terminal) interface for the arm relay status monitor.
"""
-import os
-import sys
import time
import curses
-
-import eventLog
-import bandwidthMonitor
-
from threading import RLock
from TorCtl import TorCtl
-REFRESH_RATE = 5 # seconds between redrawing screen
-cursesLock = RLock() # curses isn't thread safe and concurrency
- # bugs produce especially sinister glitches
+import util
+import staticPanel
+import bandwidthPanel
+import logPanel
-# default formatting constants
-LABEL_ATTR = curses.A_STANDOUT
-SUMMARY_ATTR = curses.A_NORMAL
+REFRESH_RATE = 5 # seconds between redrawing screen
+cursesLock = RLock() # global curses lock (curses isn't thread safe and
+ # concurrency bugs produce especially sinister glitches
-# colors curses can handle
-COLOR_LIST = (("red", curses.COLOR_RED),
- ("green", curses.COLOR_GREEN),
- ("yellow", curses.COLOR_YELLOW),
- ("blue", curses.COLOR_BLUE),
- ("cyan", curses.COLOR_CYAN),
- ("magenta", curses.COLOR_MAGENTA),
- ("black", curses.COLOR_BLACK),
- ("white", curses.COLOR_WHITE))
+CTL_HELP, CTL_PAUSED, CTL_EVENT_INPUT, CTL_EVENT_ERR = range(4) # enums for message in control label
-# TODO: change COLOR_ATTR into something that can be imported via 'from'
-# 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])
+# mapping of panels to (height, start y), -1 if unlimited
+PANEL_INFO = {
+ "summary": (6, 0), # top static content
+ "control": (1, 6), # line for user input
+ "bandwidthLabel": (1, 7), # bandwidth section label
+ "bandwidth": (8, 8), # bandwidth measurements / graph
+ "logLabel": (1, 16), # message log label
+ "log": (-1, 17)} # uses all remaining space for message log
-def getSizeLabel(bytes):
- """
- Converts byte count into label in its most significant units, for instance
- 7500 bytes would return "7 KB".
- """
+def drawControlLabel(scr, msgType, arg=""):
+ """ Draws single line label for interface controls. """
+ scr.clear()
- if bytes >= 1073741824: return "%i GB" % (bytes / 1073741824)
- elif bytes >= 1048576: return "%i MB" % (bytes / 1048576)
- elif bytes >= 1024: return "%i KB" % (bytes / 1024)
- else: return "%i bytes" % bytes
-
-def getStaticInfo(conn):
- """
- Provides mapping of static Tor settings and system information to their
- corresponding string values. Keys include:
- info - version, config-file, address, fingerprint
- sys - sys-name, sys-os, sys-version
- config - Nickname, ORPort, DirPort, ControlPort, ExitPolicy, BandwidthRate, BandwidthBurst
- config booleans - IsPasswordAuthSet, IsCookieAuthSet
- """
+ if msgType == CTL_HELP: scr.addstr(0, 0, "q: quit, e: change events, p: pause")
+ elif msgType == CTL_PAUSED: scr.addstr(0, 0, "Paused", curses.A_STANDOUT)
+ elif msgType == CTL_EVENT_INPUT: scr.addstr(0, 0, "Events to log: ")
+ elif msgType == CTL_EVENT_ERR: scr.addstr(0, 0, arg, curses.A_STANDOUT)
+ else:
+ assert False, "Unrecognized event type for control label: " + str(msgType)
- vals = conn.get_info(["version", "config-file"])
-
- # gets parameters that throw errors if unavailable
- for param in ["address", "fingerprint"]:
- try:
- vals.update(conn.get_info(param))
- except TorCtl.ErrorReply:
- vals[param] = "Unknown"
-
- # populates with some basic system information
- unameVals = os.uname()
- vals["sys-name"] = unameVals[1]
- vals["sys-os"] = unameVals[0]
- vals["sys-version"] = unameVals[2]
-
- # parameters from the user's torrc
- configFields = ["Nickname", "ORPort", "DirPort", "ControlPort", "ExitPolicy", "BandwidthRate", "BandwidthBurst"]
- vals.update(dict([(key, conn.get_option(key)[0][1]) for key in configFields]))
-
- # simply keeps booleans for if authentication info is set
- vals["IsPasswordAuthSet"] = not conn.get_option("HashedControlPassword")[0][1] == None
- vals["IsCookieAuthSet"] = conn.get_option("CookieAuthentication")[0][1] == "1"
-
- return vals
+ scr.refresh()
-def drawSummary(screen, vals, maxX, maxY):
- """
- Draws top area containing static information.
-
- arm - <System Name> (<OS> <Version>) Tor <Tor Version>
- <Relay Nickname> - <IP Addr>:<ORPort>, [Dir Port: <DirPort>, ]Control Port (<open, password, cookie>): <ControlPort>
- Fingerprint: <Fingerprint>
- Config: <Config>
- Exit Policy: <ExitPolicy>
-
- Example:
- arm - odin (Linux 2.6.24-24-generic) Tor 0.2.0.34 (r18423)
- odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
- Fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
- Config: /home/atagar/.vidalia/torrc
- Exit Policy: reject *:*
- """
-
- # extra erase/refresh is needed to avoid internal caching screwing up and
- # refusing to redisplay content in the case of graphical glitches - probably
- # an obscure curses bug...
- screen.erase()
- screen.refresh()
-
- screen.erase()
-
- # Line 1
- if maxY >= 1:
- screen.addstr(0, 0, ("arm - %s (%s %s)" % (vals["sys-name"], vals["sys-os"], vals["sys-version"]))[:maxX - 1], SUMMARY_ATTR)
- if 45 < maxX: screen.addstr(0, 45, ("Tor %s" % vals["version"])[:maxX - 46], SUMMARY_ATTR)
-
- # Line 2 (authentication label red if open, green if credentials required)
- if maxY >= 2:
- dirPortLabel = "Dir Port: %s, " % vals["DirPort"] if not vals["DirPort"] == None else ""
-
- # TODO: if both cookie and password are set then which takes priority?
- if vals["IsPasswordAuthSet"]: controlPortAuthLabel = "password"
- elif vals["IsCookieAuthSet"]: controlPortAuthLabel = "cookie"
- else: controlPortAuthLabel = "open"
- controlPortAuthColor = "red" if controlPortAuthLabel == "open" else "green"
-
- labelStart = "%s - %s:%s, %sControl Port (" % (vals["Nickname"], vals["address"], vals["ORPort"], dirPortLabel)
- screen.addstr(1, 0, labelStart[:maxX - 1], SUMMARY_ATTR)
- xLoc = len(labelStart)
- if xLoc < maxX: screen.addstr(1, xLoc, controlPortAuthLabel[:maxX - xLoc - 1], COLOR_ATTR[controlPortAuthColor] | SUMMARY_ATTR)
- xLoc += len(controlPortAuthLabel)
- if xLoc < maxX: screen.addstr(1, xLoc, ("): %s" % vals["ControlPort"])[:maxX - xLoc - 1], SUMMARY_ATTR)
-
- # Lines 3-5
- if maxY >= 3: screen.addstr(2, 0, ("Fingerprint: %s" % vals["fingerprint"])[:maxX - 1], SUMMARY_ATTR)
- if maxY >= 4: screen.addstr(3, 0, ("Config: %s" % vals["config-file"])[:maxX - 1], SUMMARY_ATTR)
- if maxY >= 5:
- exitPolicy = vals["ExitPolicy"]
-
- # adds note when default exit policy is appended
- if exitPolicy == None: exitPolicy = "<default>"
- elif not exitPolicy.endswith("accept *:*") and not exitPolicy.endswith("reject *:*"):
- exitPolicy += ", <default>"
- screen.addstr(4, 0, ("Exit Policy: %s" % exitPolicy)[:maxX - 1], SUMMARY_ATTR)
-
- screen.refresh()
-
-def drawPauseLabel(screen, isPaused, maxX):
- """ Draws single line label for interface controls. """
- # TODO: possibly include 'h: help' if the project grows much
- screen.erase()
- if isPaused: screen.addstr(0, 0, "Paused"[:maxX - 1], LABEL_ATTR)
- else: screen.addstr(0, 0, "q: quit, e: change events, p: pause"[:maxX - 1])
- screen.refresh()
-
def setEventListening(loggedEvents, conn, logListener):
"""
Tries to set events being listened for, displaying error for any event
@@ -185,10 +74,50 @@
logListener.registerEvent("ARM-ERR", "Unsupported event type: %s" % eventType, "red")
else:
raise exc
+
loggedEvents = list(loggedEvents)
loggedEvents.sort() # alphabetizes
return loggedEvents
+def refreshSubwindows(stdscr, panels={}):
+ """
+ Creates constituent parts of the display. Any subwindows that have been
+ displaced are recreated to take advantage of the maximum bounds.
+ """
+
+ y, x = stdscr.getmaxyx()
+
+ if panels == {}:
+ # initializes subwindows - upper left must be a valid coordinate
+ for panelName, (maxHeight, startY) in PANEL_INFO.items():
+ if maxHeight == -1: height = max(1, y - startY)
+ else: height = max(1, min(maxHeight, y - startY))
+ startY = min(startY, y - 1)
+ panels[panelName] = util.TermSubwindow(stdscr.subwin(height, x, startY, 0), cursesLock, startY)
+ else:
+ # I'm not sure if recreating subwindows is some sort of memory leak but the
+ # Python curses bindings seem to lack all of the following:
+ # - subwindow deletion (to tell curses to free the memory)
+ # - subwindow moving/resizing (to restore the displaced windows)
+ # so this is the only option (besides removing subwindows entirly which
+ # would mean more complicated code and no more selective refreshing)
+
+ for panelName, (maxHeight, startY) in PANEL_INFO.items():
+ if startY > y: continue # out of bounds - ignore
+ panelSrc = panels[panelName]
+ currentY, currentX = panelSrc.win.getparyx()
+ currentHeight, currentWidth = panelSrc.win.getmaxyx()
+
+ # figure out panel can grow - if so recreate
+ if maxHeight == -1: height = max(1, y - startY)
+ else: height = max(1, min(maxHeight, y - startY))
+ startY = min(startY, y - 1)
+
+ if currentY < startY or currentHeight < height:
+ panels[panelName].win = stdscr.subwin(height, x, startY, 0)
+
+ return panels
+
def drawTorMonitor(stdscr, conn, loggedEvents):
"""
Starts arm interface reflecting information on provided control port.
@@ -199,110 +128,59 @@
otherwise unrecognized events)
"""
- global COLOR_ATTR_INITIALIZED
-
- # use terminal defaults to allow things like semi-transparent backgrounds
- curses.use_default_colors()
-
- # initializes color mappings if able
- if curses.has_colors() and not COLOR_ATTR_INITIALIZED:
- COLOR_ATTR_INITIALIZED = True
- colorpair = 0
-
- for name, fgColor in COLOR_LIST:
- colorpair += 1
- curses.init_pair(colorpair, fgColor, -1) # -1 allows for default (possibly transparent) background
- COLOR_ATTR[name] = curses.color_pair(colorpair)
-
+ curses.use_default_colors() # allows things like semi-transparent backgrounds
+ util.initColors() # initalizes color pairs for colored text
curses.halfdelay(REFRESH_RATE * 10) # uses getch call as timer for REFRESH_RATE seconds
- staticInfo = getStaticInfo(conn)
- y, x = stdscr.getmaxyx()
- oldX, oldY = -1, -1
# attempts to make the cursor invisible (not supported in all terminals)
try: curses.curs_set(0)
except curses.error: pass
- # note: subwindows need a character buffer (either in the x or y direction)
- # from actual content to prevent crash when shrank
- # max/min calls are to allow the program to initialize if the screens too small
- summaryScreen = stdscr.subwin(min(y, 6), x, 0, 0) # top static content
- pauseLabel = stdscr.subwin(1, x, min(y - 1, 6), 0) # line concerned with user interface
- bandwidthLabel = stdscr.subwin(1, x, min(y - 1, 7), 0) # bandwidth section label
- bandwidthScreen = stdscr.subwin(min(y - 8, 8), x, min(y - 2, 8), 0) # bandwidth measurements / graph
- logLabel = stdscr.subwin(1, x, min(y - 1, 16), 0) # message log label
- logScreen = stdscr.subwin(max(1, y - 17), x, min(y - 1, 17), 0) # uses all remaining space for message log
+ panels = refreshSubwindows(stdscr)
+ staticInfo = staticPanel.getStaticInfo(conn)
- # listeners that update bandwidthScreen and logScreen with Tor statuses
- logListener = eventLog.LogMonitor(logScreen, "BW" in loggedEvents, "UNKNOWN" in loggedEvents, cursesLock)
+ # listeners that update bandwidth and log panels with Tor status
+ logListener = logPanel.LogMonitor(panels["log"], "BW" in loggedEvents, "UNKNOWN" in loggedEvents)
conn.add_event_listener(logListener)
- bandwidthListener = bandwidthMonitor.BandwidthMonitor(bandwidthScreen, cursesLock)
+ bandwidthListener = bandwidthPanel.BandwidthMonitor(panels["bandwidth"])
conn.add_event_listener(bandwidthListener)
loggedEvents = setEventListening(loggedEvents, conn, logListener)
eventsListing = ", ".join(loggedEvents)
-
- bandwidthScreen.refresh()
- logScreen.refresh()
+ oldY, oldX = -1, -1
isPaused = False
- tick = -1 # loop iteration
while True:
- tick += 1
- y, x = stdscr.getmaxyx()
+ # tried only refreshing when the screen was resized but it caused a
+ # noticeable lag when resizing and didn't have an appreciable effect
+ # on system usage
- if x != oldX or y != oldY or tick % 5 == 0:
- # resized - redraws content
- # occasionally refreshes anyway to help make resilient against an
- # occasional graphical glitch - currently only known cause of this is
- # displaced subwindows overwritting static content when resized to be
- # very small
+ cursesLock.acquire()
+ try:
+ y, x = stdscr.getmaxyx()
+ if y > oldY: panels = refreshSubwindows(stdscr, panels)
- # note: Having this refresh only occure after this resize is detected
- # (getmaxyx changes) introduces a noticeable lag in screen updates. If
- # it's done every pass through the loop resize repaint responsiveness is
- # perfect, but this is much more demanding in the common case (no resizing).
- cursesLock.acquire()
+ staticPanel.drawSummary(panels["summary"], staticInfo)
- try:
- if y > oldY:
- # screen height increased - recreates subwindows that are able to grow
- # I'm not sure if this is some sort of memory leak but the Python curses
- # bindings seem to lack all of the following:
- # - subwindow deletion (to tell curses to free the memory)
- # - subwindow moving/resizing (to restore the displaced windows)
- # so this is the only option (besides removing subwindows entirly which
- # would mean more complicated code and no more selective refreshing)
-
- # TODO: BUG - this fails if doing maximize operation (relies on gradual growth/shrink -
- # fix would be to eliminate elif and consult new y)
- if oldY < 6: summaryScreen = stdscr.subwin(y, x, 0, 0)
- elif oldY < 7: pauseLabel = stdscr.subwin(1, x, 6, 0)
- elif oldY < 8: bandwidthLabel = stdscr.subwin(1, x, 7, 0)
- elif oldY < 16:
- bandwidthScreen = stdscr.subwin(y - 8, x, 8, 0)
- bandwidthListener.bandwidthScreen = bandwidthScreen
- elif oldY < 17: logLabel = stdscr.subwin(1, x, 16, 0)
- else:
- logScreen = stdscr.subwin(y - 17, x, 17, 0)
- logListener.logScreen = logScreen
-
- drawSummary(summaryScreen, staticInfo, x, y)
- drawPauseLabel(pauseLabel, isPaused, x)
- bandwidthMonitor.drawBandwidthLabel(bandwidthLabel, staticInfo, x)
- bandwidthListener.refreshDisplay()
- eventLog.drawEventLogLabel(logLabel, eventsListing, x)
- logListener.refreshDisplay()
- oldX, oldY = x, y
- stdscr.refresh()
- finally:
- cursesLock.release()
+ msgType = CTL_PAUSED if isPaused else CTL_HELP
+ drawControlLabel(panels["control"], msgType)
+
+ bandwidthPanel.drawBandwidthLabel(panels["bandwidthLabel"], staticInfo)
+ bandwidthListener.refreshDisplay()
+
+ logPanel.drawEventLogLabel(panels["logLabel"], eventsListing)
+ logListener.refreshDisplay()
+
+ oldY, oldX = y, x
+ stdscr.refresh()
+ finally:
+ cursesLock.release()
key = stdscr.getch()
- if key == ord('q') or key == ord('Q'): break # quits
+ if key == 27 or key == ord('q') or key == ord('Q'): break # quits (also on esc)
elif key == ord('e') or key == ord('E'):
- # allow user to enter new types of events to log - blank leaves unchanged
+ # allow user to enter new types of events to log - unchanged if left blank
cursesLock.acquire()
try:
@@ -314,9 +192,7 @@
logListener.setPaused(True)
# provides prompt
- pauseLabel.erase()
- pauseLabel.addstr(0, 0, "Events to log: "[:x - 1])
- pauseLabel.refresh()
+ drawControlLabel(panels["control"], CTL_EVENT_INPUT)
# makes cursor and typing visible
try: curses.curs_set(1)
@@ -324,22 +200,20 @@
curses.echo()
# switches bandwidth area to list event types
- bandwidthLabel.erase()
- bandwidthLabel.addstr(0, 0, "Event Types:"[:x - 1], LABEL_ATTR)
- bandwidthLabel.refresh()
+ panels["bandwidthLabel"].clear()
+ panels["bandwidthLabel"].addstr(0, 0, "Event Types:", util.LABEL_ATTR)
+ panels["bandwidthLabel"].refresh()
- bandwidthScreen.erase()
- bandwidthScreenMaxY, tmp = bandwidthScreen.getmaxyx()
+ panels["bandwidth"].clear()
lineNum = 0
- for line in eventLog.EVENT_LISTING.split("\n"):
+ for line in logPanel.EVENT_LISTING.split("\n"):
line = line.strip()
- if bandwidthScreenMaxY <= lineNum: break
- bandwidthScreen.addstr(lineNum, 0, line[:x - 1])
+ panels["bandwidth"].addstr(lineNum, 0, line[:x - 1])
lineNum += 1
- bandwidthScreen.refresh()
+ panels["bandwidth"].refresh()
# gets user input (this blocks monitor updates)
- eventsInput = pauseLabel.getstr(0, 15)
+ eventsInput = panels["control"].win.getstr(0, 15)
# strips spaces
eventsInput = eventsInput.replace(' ', '')
@@ -352,27 +226,21 @@
# TODO: it would be nice to quit on esc, but looks like this might not be possible...
if eventsInput != "":
try:
- expandedEvents = eventLog.expandEvents(eventsInput)
+ expandedEvents = logPanel.expandEvents(eventsInput)
logListener.includeBW = "BW" in expandedEvents
logListener.includeUnknown = "UNKNOWN" in expandedEvents
loggedEvents = setEventListening(expandedEvents, conn, logListener)
eventsListing = ", ".join(loggedEvents)
except ValueError, exc:
- pauseLabel.erase()
- pauseLabel.addstr(0, 0, ("Invalid flags: %s" % str(exc))[:x - 1], curses.A_STANDOUT)
- pauseLabel.refresh()
+ drawControlLabel(panels["control"], CTL_EVENT_ERR, "Invalid flags: %s" % str(exc))
time.sleep(2)
# returns listeners to previous pause status
bandwidthListener.setPaused(isBwPaused)
logListener.setPaused(isLogPaused)
-
- oldX = -1 # forces refresh (by spoofing a resize)
finally:
cursesLock.release()
-
-
elif key == ord('p') or key == ord('P'):
# toggles update freezing
cursesLock.acquire()
@@ -380,7 +248,8 @@
isPaused = not isPaused
logListener.setPaused(isPaused)
bandwidthListener.setPaused(isPaused)
- drawPauseLabel(pauseLabel, isPaused, x)
+ msgType = CTL_PAUSED if isPaused else CTL_HELP
+ drawControlLabel(panels["control"], msgType)
finally:
cursesLock.release()
Deleted: arm/trunk/interface/eventLog.py
===================================================================
--- arm/trunk/interface/eventLog.py 2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/interface/eventLog.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -1,222 +0,0 @@
-#!/usr/bin/env python
-# eventLog.py -- Resources related to Tor event monitoring.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-import curses
-import controller
-from curses.ascii import isprint
-from TorCtl import TorCtl
-
-MAX_LOG_ENTRIES = 80 # size of log buffer (max number of entries)
-RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
-
-EVENT_TYPES = {
- "d": "DEBUG", "a": "ADDRMAP", "l": "NEWDESC", "u": "AUTHDIR_NEWDESCS",
- "i": "INFO", "b": "BW", "m": "NS", "v": "CLIENTS_SEEN",
- "n": "NOTICE", "c": "CIRC", "o": "ORCONN", "x": "STATUS_GENERAL",
- "w": "WARN", "f": "DESCCHANGED", "s": "STREAM", "y": "STATUS_CLIENT",
- "e": "ERR", "g": "GUARD", "t": "STREAM_BW", "z": "STATUS_SERVER"}
-
-EVENT_LISTING = """ d DEBUG a ADDRMAP l NEWDESC u AUTHDIR_NEWDESCS
- i INFO b BW m NS v CLIENTS_SEEN
- n NOTICE c CIRC o ORCONN x STATUS_GENERAL
- w WARN f DESCCHANGED s STREAM y STATUS_CLIENT
- e ERR g GUARD t STREAM_BW z STATUS_SERVER
- Aliases: A All Events U Unknown Events R Runlevels (dinwe)"""
-
-def expandEvents(eventAbbr):
- """
- Expands event abbreviations to their full names. Beside mappings privided in
- EVENT_TYPES this recognizes:
- A - alias for all events
- U - "UNKNOWN" events
- R - alias for runtime events (DEBUG, INFO, NOTICE, WARN, ERR)
- Raises ValueError with invalid input if any part isn't recognized.
- """
-
- expandedEvents = set()
- invalidFlags = ""
- for flag in eventAbbr:
- if flag == "A":
- expandedEvents = set(EVENT_TYPES.values())
- expandedEvents.add("UNKNOWN")
- break
- elif flag == "U":
- expandedEvents.add("UNKNOWN")
- elif flag == "R":
- expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
- elif flag in EVENT_TYPES:
- expandedEvents.add(EVENT_TYPES[flag])
- else:
- invalidFlags += flag
-
- if invalidFlags: raise ValueError(invalidFlags)
- else: return expandedEvents
-
-def drawEventLogLabel(screen, eventsListing, maxX):
- """
- Draws single line label for event log. Uses ellipsis if too long, for instance:
- Events (DEBUG, INFO, NOTICE, WARN...):
- """
-
- eventsLabel = "Events"
-
- firstLabelLen = eventsListing.find(", ")
- if firstLabelLen == -1: firstLabelLen = len(eventsListing)
- else: firstLabelLen += 3
-
- if maxX > 10 + firstLabelLen:
- eventsLabel += " ("
- if len(eventsListing) > maxX - 11:
- labelBreak = eventsListing[:maxX - 12].rfind(", ")
- eventsLabel += "%s..." % eventsListing[:labelBreak]
- else: eventsLabel += eventsListing
- eventsLabel += ")"
- eventsLabel += ":"
-
- screen.erase()
- # not sure why but when stressed this call sometimes fails
- try: screen.addstr(0, 0, eventsLabel[:maxX - 1], controller.LABEL_ATTR)
- except curses.error: pass
- screen.refresh()
-
-class LogMonitor(TorCtl.PostEventListener):
- """
- Tor event listener, noting messages, the time, and their type in a curses
- subwindow.
- """
-
- def __init__(self, logScreen, includeBW, includeUnknown, cursesLock):
- TorCtl.PostEventListener.__init__(self)
- self.msgLog = [] # tuples of (logText, color)
- self.logScreen = logScreen # curses window where log's displayed
- self.isPaused = False
- self.pauseBuffer = [] # location where messages are buffered if paused
- self.includeBW = includeBW # true if we're supposed to listen for BW events
- self.includeUnknown = includeUnknown # true if registering unrecognized events
- self.cursesLock = cursesLock # lock to safely use logScreen
-
- # Listens for all event types and redirects to registerEvent
- # TODO: not sure how to stimulate all event types - should be tried before
- # implemented to see what's the best formatting, what information is
- # important, and to make sure of variable's types so we don't get exceptions.
-
- def circ_status_event(self, event):
- self.registerEvent("CIRC", "<STUB>", "white") # TODO: implement - variables: event.circ_id, event.status, event.path, event.purpose, event.reason, event.remote_reason
-
- def stream_status_event(self, event):
- self.registerEvent("STREAM", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose
-
- def or_conn_status_event(self, event):
- self.registerEvent("ORCONN", "<STUB>", "white") # TODO: implement - variables: event.status, event.endpoint, event.age, event.read_bytes, event.wrote_bytes, event.reason, event.ncircs
-
- def stream_bw_event(self, event):
- self.registerEvent("STREAM_BW", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.bytes_read, event.bytes_written
-
- def bandwidth_event(self, event):
- if self.includeBW: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
-
- def msg_event(self, event):
- self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
-
- def new_desc_event(self, event):
- self.registerEvent("NEWDESC", "<STUB>", "white") # TODO: implement - variables: event.idlist
-
- def address_mapped_event(self, event):
- self.registerEvent("ADDRMAP", "<STUB>", "white") # TODO: implement - variables: event.from_addr, event.to_addr, event.when
-
- def ns_event(self, event):
- self.registerEvent("NS", "<STUB>", "white") # TODO: implement - variables: event.nslist
-
- def new_consensus_event(self, event):
- self.registerEvent("NEWCONSENSUS", "<STUB>", "white") # TODO: implement - variables: event.nslist
-
- def unknown_event(self, event):
- if self.includeUnknown: self.registerEvent("UNKNOWN", event.event_string, "red")
-
- def registerEvent(self, type, msg, color):
- """
- Notes event and redraws log. If paused it's held in a temporary buffer.
- """
-
- # strips control characters to avoid screwing up the terminal
- msg = "".join([char for char in msg if isprint(char)])
-
- eventTime = time.localtime()
- msgLine = "%02i:%02i:%02i [%s] %s" % (eventTime[3], eventTime[4], eventTime[5], type, msg)
-
- if self.isPaused:
- self.pauseBuffer.insert(0, (msgLine, color))
- if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
- else:
- self.msgLog.insert(0, (msgLine, color))
- if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
- self.refreshDisplay()
-
- def refreshDisplay(self):
- """
- Redraws message log. Entries stretch to use available space and may
- contain up to two lines. Starts with newest entries.
- """
-
- if self.logScreen:
- if not self.cursesLock.acquire(False): return
- try:
- self.logScreen.erase()
- y, x = self.logScreen.getmaxyx()
- lineCount = 0
-
- for (line, color) in self.msgLog:
- # splits over too lines if too long
- if len(line) < x:
- self.logScreen.addstr(lineCount, 0, line[:x - 1], controller.COLOR_ATTR[color])
- lineCount += 1
- else:
- if lineCount >= y - 1: break
- (line1, line2) = self._splitLine(line, x)
- self.logScreen.addstr(lineCount, 0, line1, controller.COLOR_ATTR[color])
- self.logScreen.addstr(lineCount + 1, 0, line2[:x - 1], controller.COLOR_ATTR[color])
- lineCount += 2
-
- if lineCount >= y: break
- self.logScreen.refresh()
- finally:
- self.cursesLock.release()
-
- def setPaused(self, isPause):
- """
- If true, prevents message log from being updated with new events.
- """
-
- if isPause == self.isPaused: return
-
- self.isPaused = isPause
- if self.isPaused: self.pauseBuffer = []
- else:
- self.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
- self.refreshDisplay()
-
- # 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()
-
- # ends line with ellipsis if too long
- if len(line2) > x:
- lastWordbreak = line2[:x - 4].rfind(" ")
-
- # doesn't use wordbreak if it's a long word or the whole line is one
- # word (picking up on two space indent to have index 1)
- if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
- line2 = "%s..." % line2[:lastWordbreak]
-
- return (line1, line2)
-
Copied: arm/trunk/interface/logPanel.py (from rev 19594, arm/trunk/interface/eventLog.py)
===================================================================
--- arm/trunk/interface/logPanel.py (rev 0)
+++ arm/trunk/interface/logPanel.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+# logPanel.py -- Resources related to Tor event monitoring.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import time
+import curses
+from curses.ascii import isprint
+from TorCtl import TorCtl
+
+import util
+
+MAX_LOG_ENTRIES = 80 # size of log buffer (max number of entries)
+RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
+
+EVENT_TYPES = {
+ "d": "DEBUG", "a": "ADDRMAP", "l": "NEWDESC", "u": "AUTHDIR_NEWDESCS",
+ "i": "INFO", "b": "BW", "m": "NS", "v": "CLIENTS_SEEN",
+ "n": "NOTICE", "c": "CIRC", "o": "ORCONN", "x": "STATUS_GENERAL",
+ "w": "WARN", "f": "DESCCHANGED", "s": "STREAM", "y": "STATUS_CLIENT",
+ "e": "ERR", "g": "GUARD", "t": "STREAM_BW", "z": "STATUS_SERVER"}
+
+EVENT_LISTING = """ d DEBUG a ADDRMAP l NEWDESC u AUTHDIR_NEWDESCS
+ i INFO b BW m NS v CLIENTS_SEEN
+ n NOTICE c CIRC o ORCONN x STATUS_GENERAL
+ w WARN f DESCCHANGED s STREAM y STATUS_CLIENT
+ e ERR g GUARD t STREAM_BW z STATUS_SERVER
+ Aliases: A All Events U Unknown Events R Runlevels (dinwe)"""
+
+def expandEvents(eventAbbr):
+ """
+ Expands event abbreviations to their full names. Beside mappings privided in
+ EVENT_TYPES this recognizes:
+ A - alias for all events
+ U - "UNKNOWN" events
+ R - alias for runtime events (DEBUG, INFO, NOTICE, WARN, ERR)
+ Raises ValueError with invalid input if any part isn't recognized.
+
+ Example:
+ "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"]
+ """
+
+ expandedEvents = set()
+ invalidFlags = ""
+ for flag in eventAbbr:
+ if flag == "A":
+ expandedEvents = set(EVENT_TYPES.values())
+ expandedEvents.add("UNKNOWN")
+ break
+ elif flag == "U":
+ expandedEvents.add("UNKNOWN")
+ elif flag == "R":
+ expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
+ elif flag in EVENT_TYPES:
+ expandedEvents.add(EVENT_TYPES[flag])
+ else:
+ invalidFlags += flag
+
+ if invalidFlags: raise ValueError(invalidFlags)
+ else: return expandedEvents
+
+def drawEventLogLabel(scr, eventsListing):
+ """
+ Draws single line label for event log. Uses ellipsis if too long, for instance:
+ Events (DEBUG, INFO, NOTICE, WARN...):
+ """
+ scr.clear()
+ maxX = scr.maxX
+
+ eventsLabel = "Events"
+
+ firstLabelLen = eventsListing.find(", ")
+ if firstLabelLen == -1: firstLabelLen = len(eventsListing)
+ else: firstLabelLen += 3
+
+ if maxX > 10 + firstLabelLen:
+ eventsLabel += " ("
+ if len(eventsListing) > maxX - 11:
+ labelBreak = eventsListing[:maxX - 12].rfind(", ")
+ eventsLabel += "%s..." % eventsListing[:labelBreak]
+ else: eventsLabel += eventsListing
+ eventsLabel += ")"
+ eventsLabel += ":"
+
+ scr.addstr(0, 0, eventsLabel, util.LABEL_ATTR)
+ scr.refresh()
+
+class LogMonitor(TorCtl.PostEventListener):
+ """
+ Tor event listener, noting messages, the time, and their type in a curses
+ subwindow.
+ """
+
+ def __init__(self, scr, includeBW, includeUnknown):
+ TorCtl.PostEventListener.__init__(self)
+ self.scr = scr # associated subwindow
+ self.msgLog = [] # tuples of (logText, color)
+ self.isPaused = False
+ self.pauseBuffer = [] # location where messages are buffered if paused
+ self.includeBW = includeBW # true if we're supposed to listen for BW events
+ self.includeUnknown = includeUnknown # true if registering unrecognized events
+
+ # Listens for all event types and redirects to registerEvent
+ # TODO: not sure how to stimulate all event types - should be tried before
+ # implemented to see what's the best formatting, what information is
+ # important, and to make sure of variable's types so we don't get exceptions.
+
+ def circ_status_event(self, event):
+ self.registerEvent("CIRC", "<STUB>", "white") # TODO: implement - variables: event.circ_id, event.status, event.path, event.purpose, event.reason, event.remote_reason
+
+ def stream_status_event(self, event):
+ self.registerEvent("STREAM", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose
+
+ def or_conn_status_event(self, event):
+ self.registerEvent("ORCONN", "<STUB>", "white") # TODO: implement - variables: event.status, event.endpoint, event.age, event.read_bytes, event.wrote_bytes, event.reason, event.ncircs
+
+ def stream_bw_event(self, event):
+ self.registerEvent("STREAM_BW", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.bytes_read, event.bytes_written
+
+ def bandwidth_event(self, event):
+ if self.includeBW: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
+
+ def msg_event(self, event):
+ self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
+
+ def new_desc_event(self, event):
+ self.registerEvent("NEWDESC", "<STUB>", "white") # TODO: implement - variables: event.idlist
+
+ def address_mapped_event(self, event):
+ self.registerEvent("ADDRMAP", "<STUB>", "white") # TODO: implement - variables: event.from_addr, event.to_addr, event.when
+
+ def ns_event(self, event):
+ self.registerEvent("NS", "<STUB>", "white") # TODO: implement - variables: event.nslist
+
+ def new_consensus_event(self, event):
+ self.registerEvent("NEWCONSENSUS", "<STUB>", "white") # TODO: implement - variables: event.nslist
+
+ def unknown_event(self, event):
+ if self.includeUnknown: self.registerEvent("UNKNOWN", event.event_string, "red")
+
+ def registerEvent(self, type, msg, color):
+ """
+ Notes event and redraws log. If paused it's held in a temporary buffer.
+ """
+
+ # strips control characters to avoid screwing up the terminal
+ msg = "".join([char for char in msg if isprint(char)])
+
+ eventTime = time.localtime()
+ msgLine = "%02i:%02i:%02i [%s] %s" % (eventTime[3], eventTime[4], eventTime[5], type, msg)
+
+ if self.isPaused:
+ self.pauseBuffer.insert(0, (msgLine, color))
+ if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
+ else:
+ self.msgLog.insert(0, (msgLine, color))
+ if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
+ self.refreshDisplay()
+
+ def refreshDisplay(self):
+ """
+ Redraws message log. Entries stretch to use available space and may
+ contain up to two lines. Starts with newest entries.
+ """
+
+ if self.scr:
+ if not self.scr.lock.acquire(False): return
+ try:
+ self.scr.clear()
+ x, y = self.scr.maxX, self.scr.maxY
+ lineCount = 0
+
+ for (line, color) in self.msgLog:
+ # splits over too lines if too long
+ if len(line) < x:
+ self.scr.addstr(lineCount, 0, line, util.getColor(color))
+ lineCount += 1
+ else:
+ (line1, line2) = self._splitLine(line, x)
+ self.scr.addstr(lineCount, 0, line1, util.getColor(color))
+ self.scr.addstr(lineCount + 1, 0, line2, util.getColor(color))
+ lineCount += 2
+
+ if lineCount >= y: break # further log messages wouldn't fit
+ self.scr.refresh()
+ finally:
+ self.scr.lock.release()
+
+ def setPaused(self, isPause):
+ """
+ If true, prevents message log from being updated with new events.
+ """
+
+ if isPause == self.isPaused: return
+
+ self.isPaused = isPause
+ if self.isPaused: self.pauseBuffer = []
+ else:
+ self.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
+ self.refreshDisplay()
+
+ # 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()
+
+ # 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)
+
Added: arm/trunk/interface/staticPanel.py
===================================================================
--- arm/trunk/interface/staticPanel.py (rev 0)
+++ arm/trunk/interface/staticPanel.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# summaryPanel.py -- Static system and Tor related information.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import curses
+from TorCtl import TorCtl
+
+import util
+
+def getStaticInfo(conn):
+ """
+ Provides mapping of static Tor settings and system information to their
+ corresponding string values. Keys include:
+ info - version, config-file, address, fingerprint
+ sys - sys-name, sys-os, sys-version
+ config - Nickname, ORPort, DirPort, ControlPort, ExitPolicy, BandwidthRate, BandwidthBurst
+ config booleans - IsPasswordAuthSet, IsCookieAuthSet
+ """
+
+ vals = conn.get_info(["version", "config-file"])
+
+ # gets parameters that throw errors if unavailable
+ for param in ["address", "fingerprint"]:
+ try:
+ vals.update(conn.get_info(param))
+ except TorCtl.ErrorReply:
+ vals[param] = "Unknown"
+
+ # populates with some basic system information
+ unameVals = os.uname()
+ vals["sys-name"] = unameVals[1]
+ vals["sys-os"] = unameVals[0]
+ vals["sys-version"] = unameVals[2]
+
+ # parameters from the user's torrc
+ configFields = ["Nickname", "ORPort", "DirPort", "ControlPort", "ExitPolicy", "BandwidthRate", "BandwidthBurst"]
+ vals.update(dict([(key, conn.get_option(key)[0][1]) for key in configFields]))
+
+ # simply keeps booleans for if authentication info is set
+ vals["IsPasswordAuthSet"] = not conn.get_option("HashedControlPassword")[0][1] == None
+ vals["IsCookieAuthSet"] = conn.get_option("CookieAuthentication")[0][1] == "1"
+
+ return vals
+
+def drawSummary(scr, vals):
+ """
+ Draws top area containing static information.
+
+ arm - <System Name> (<OS> <Version>) Tor <Tor Version>
+ <Relay Nickname> - <IP Addr>:<ORPort>, [Dir Port: <DirPort>, ]Control Port (<open, password, cookie>): <ControlPort>
+ Fingerprint: <Fingerprint>
+ Config: <Config>
+ Exit Policy: <ExitPolicy>
+
+ Example:
+ arm - odin (Linux 2.6.24-24-generic) Tor 0.2.0.34 (r18423)
+ odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
+ Fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
+ Config: /home/atagar/.vidalia/torrc
+ Exit Policy: reject *:*
+ """
+
+ # extra erase/refresh is needed to avoid internal caching screwing up and
+ # refusing to redisplay content in the case of graphical glitches - probably
+ # an obscure curses bug...
+ scr.win.erase()
+ scr.win.refresh()
+
+ scr.clear()
+
+ # Line 1
+ scr.addstr(0, 0, "arm - %s (%s %s)" % (vals["sys-name"], vals["sys-os"], vals["sys-version"]))
+ scr.addstr(0, 45, "Tor %s" % vals["version"])
+
+ # Line 2 (authentication label red if open, green if credentials required)
+ dirPortLabel = "Dir Port: %s, " % vals["DirPort"] if not vals["DirPort"] == None else ""
+
+ # TODO: if both cookie and password are set then which takes priority?
+ if vals["IsPasswordAuthSet"]: controlPortAuthLabel = "password"
+ elif vals["IsCookieAuthSet"]: controlPortAuthLabel = "cookie"
+ else: controlPortAuthLabel = "open"
+ controlPortAuthColor = "red" if controlPortAuthLabel == "open" else "green"
+
+ labelStart = "%s - %s:%s, %sControl Port (" % (vals["Nickname"], vals["address"], vals["ORPort"], dirPortLabel)
+ scr.addstr(1, 0, labelStart)
+ xLoc = len(labelStart)
+ scr.addstr(1, xLoc, controlPortAuthLabel, util.getColor(controlPortAuthColor))
+ xLoc += len(controlPortAuthLabel)
+ scr.addstr(1, xLoc, "): %s" % vals["ControlPort"])
+
+ # Lines 3-5
+ scr.addstr(2, 0, "Fingerprint: %s" % vals["fingerprint"])
+ scr.addstr(3, 0, "Config: %s" % vals["config-file"])
+ exitPolicy = vals["ExitPolicy"]
+
+ # adds note when default exit policy is appended
+ if exitPolicy == None: exitPolicy = "<default>"
+ elif not exitPolicy.endswith("accept *:*") and not exitPolicy.endswith("reject *:*"):
+ exitPolicy += ", <default>"
+ scr.addstr(4, 0, "Exit Policy: %s" % exitPolicy)
+
+ scr.refresh()
+
Added: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py (rev 0)
+++ arm/trunk/interface/util.py 2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+# util.py -- support functions common for arm user interface.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import curses
+
+LABEL_ATTR = curses.A_STANDOUT # default formatting constant
+
+# colors curses can handle
+COLOR_LIST = (("red", curses.COLOR_RED),
+ ("green", curses.COLOR_GREEN),
+ ("yellow", curses.COLOR_YELLOW),
+ ("blue", curses.COLOR_BLUE),
+ ("cyan", curses.COLOR_CYAN),
+ ("magenta", curses.COLOR_MAGENTA),
+ ("black", curses.COLOR_BLACK),
+ ("white", curses.COLOR_WHITE))
+
+# 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])
+
+def initColors():
+ """
+ Initializes color mappings for the current curses. This needs to be called
+ after curses.initscr().
+ """
+
+ global COLOR_ATTR_INITIALIZED
+ if not COLOR_ATTR_INITIALIZED:
+ COLOR_ATTR_INITIALIZED = True
+
+ # if color support is available initializes color mappings
+ if curses.has_colors():
+ colorpair = 0
+
+ for name, fgColor in COLOR_LIST:
+ colorpair += 1
+ curses.init_pair(colorpair, fgColor, -1) # -1 allows for default (possibly transparent) background
+ COLOR_ATTR[name] = curses.color_pair(colorpair)
+
+def getColor(color):
+ """
+ Provides attribute corresponding to a given text color. Supported colors
+ include:
+ red, green, yellow, blue, cyan, magenta, black, and white
+
+ If color support isn't available then this uses the default terminal coloring
+ scheme.
+ """
+
+ return COLOR_ATTR[color]
+
+def getSizeLabel(bytes):
+ """
+ Converts byte count into label in its most significant units, for instance
+ 7500 bytes would return "7 KB".
+ """
+
+ if bytes >= 1073741824: return "%i GB" % (bytes / 1073741824)
+ elif bytes >= 1048576: return "%i MB" % (bytes / 1048576)
+ elif bytes >= 1024: return "%i KB" % (bytes / 1024)
+ else: return "%i bytes" % bytes
+
+class TermSubwindow():
+ """
+ Wrapper for curses subwindows. This provides safe proxies to common methods.
+ """
+
+ def __init__(self, win, lock, startY):
+ self.win = win # associated curses subwindow
+ self.lock = lock # global curses lock
+ self.startY = startY # y-coordinate where made
+ self.disable = False # set if we detect being displaced
+ self._resetBounds() # sets last known dimensions of win
+
+ def clear(self):
+ """
+ Erases window and resets bounds used in writting to it.
+ """
+
+ self.disable = self.startY > self.win.getparyx()[0]
+ if not self.disable: self.win.erase()
+ self._resetBounds()
+
+ def refresh(self):
+ """
+ Proxy for window refresh.
+ """
+
+ if not self.disable: self.win.refresh()
+
+ def addstr(self, y, x, msg, attr=curses.A_NORMAL):
+ """
+ Writes string to subwindow if able. This takes into account screen bounds
+ to avoid making curses upset.
+ """
+
+ # subwindows need a character buffer (either in the x or y direction) from
+ # actual content to prevent crash when shrank
+ if self.maxX > x and self.maxY > y:
+ if not self.disable: self.win.addstr(y, x, msg[:self.maxX - x - 1], attr)
+
+ def _resetBounds(self):
+ self.maxY, self.maxX = self.win.getmaxyx()
+