[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