[tor-commits] [arm/master] Adding a dialog for showing usage stats

atagar at torproject.org atagar at torproject.org
Wed Aug 17 17:18:21 UTC 2011


commit 4e61f4660c8c5bd5b5fa8a045dd2a39deada65cc
Author: Damian Johnson <atagar at torproject.org>
Date:   Wed Aug 17 10:16:13 2011 -0700

    Adding a dialog for showing usage stats
    
    Dialog is enabled for bridges, guards, and exits to show client locale and exit
    port usage statistics.
---
 README                            |    1 +
 src/cli/connections/__init__.py   |    2 +-
 src/cli/connections/connPanel.py  |   74 ++++++++++++++++++++++++++++++-
 src/cli/connections/countPopup.py |   86 +++++++++++++++++++++++++++++++++++++
 4 files changed, 159 insertions(+), 4 deletions(-)

diff --git a/README b/README
index 119d24e..cbaf31a 100644
--- a/README
+++ b/README
@@ -174,6 +174,7 @@ Layout:
         connPanel.py       - (page 2) lists the active tor connections
         circEntry.py       - circuit entries in the connection panel
         connEntry.py       - individual connections to or from the system
+        countPopup.py      - displays client locale or exit port counts
         descriptorPopup.py - displays raw descriptor and consensus entries
         entries.py         - common parent for connPanel display entries
       
diff --git a/src/cli/connections/__init__.py b/src/cli/connections/__init__.py
index 0f29d23..abd3410 100644
--- a/src/cli/connections/__init__.py
+++ b/src/cli/connections/__init__.py
@@ -2,5 +2,5 @@
 Connection panel related resources.
 """
 
-__all__ = ["circEntry", "connEntry", "connPanel", "descriptorPopup", "entries"]
+__all__ = ["circEntry", "connEntry", "connPanel", "countPopup", "descriptorPopup", "entries"]
 
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 923902c..9745176 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -2,13 +2,14 @@
 Listing of the currently established connections tor has made.
 """
 
+import re
 import time
 import curses
 import threading
 
 import cli.popups
 
-from cli.connections import descriptorPopup, entries, connEntry, circEntry
+from cli.connections import countPopup, descriptorPopup, entries, connEntry, circEntry
 from util import connections, enum, panel, torTools, uiTools
 
 DEFAULT_CONFIG = {"features.connection.resolveApps": True,
@@ -68,6 +69,32 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     self._cond = threading.Condition()  # used for pausing the thread
     self.valsLock = threading.RLock()
     
+    # Tracks exiting port and client country statistics
+    self._clientLocaleUsage = {}
+    self._exitPortUsage = {}
+    
+    # If we're a bridge and been running over a day then prepopulates with the
+    # last day's clients.
+    
+    conn = torTools.getConn()
+    bridgeClients = conn.getInfo("status/clients-seen")
+    
+    if bridgeClients:
+      # Response has a couple arguments...
+      # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8
+      
+      countrySummary = None
+      for arg in bridgeClients.split():
+        if arg.startswith("CountrySummary="):
+          countrySummary = arg[15:]
+          break
+      
+      if countrySummary:
+        for entry in countrySummary.split(","):
+          if re.match("^..=[0-9]+$", entry):
+            locale, count = entry.split("=", 1)
+            self._clientLocaleUsage[locale] = int(count)
+    
     # Last sampling received from the ConnectionResolver, used to detect when
     # it changes.
     self._lastResourceFetch = -1
@@ -84,7 +111,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         entry.getLines()[0].isInitialConnection = True
     
     # listens for when tor stops so we know to stop reflecting changes
-    torTools.getConn().addStatusListener(self.torStateListener)
+    conn.addStatusListener(self.torStateListener)
   
   def torStateListener(self, conn, eventType):
     """
@@ -155,6 +182,22 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     
     self.valsLock.release()
   
+  def isClientsAllowed(self):
+    """
+    True if client connections are permissable, false otherwise.
+    """
+    
+    conn = torTools.getConn()
+    return "Guard" in conn.getMyFlags([]) or conn.getOption("BridgeRelay") == "1"
+  
+  def isExitsAllowed(self):
+    """
+    True if exit connections are permissable, false otherwise.
+    """
+    
+    policy = torTools.getConn().getExitPolicy()
+    return policy and policy.isExitingAllowed()
+  
   def showSortDialog(self):
     """
     Provides the sort dialog for our connections.
@@ -214,6 +257,10 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     elif key == ord('d') or key == ord('D'):
       # presents popup for raw consensus data
       descriptorPopup.showDescriptorPopup(self)
+    elif (key == ord('c') or key == ord('C')) and self.isClientsAllowed():
+      countPopup.showCountDialog(countPopup.CountType.CLIENT_LOCALE, self._clientLocaleUsage)
+    elif (key == ord('e') or key == ord('E')) and self.isExitsAllowed():
+      countPopup.showCountDialog(countPopup.CountType.EXIT_PORT, self._exitPortUsage)
     else: isKeystrokeConsumed = False
     
     self.valsLock.release()
@@ -263,6 +310,13 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     options.append(("page down", "scroll down a page", None))
     options.append(("enter", "edit configuration option", None))
     options.append(("d", "raw consensus descriptor", None))
+    
+    if self.isClientsAllowed():
+      options.append(("c", "client locale usage summary", None))
+    
+    if self.isExitsAllowed():
+      options.append(("e", "exit port usage summary", None))
+    
     options.append(("l", "listed identity", self._listingType.lower()))
     options.append(("s", "sort ordering", None))
     options.append(("u", "resolving utility", resolverUtil))
@@ -411,8 +465,22 @@ class ConnectionPanel(panel.Panel, threading.Thread):
       # Adds any new connection and circuit entries.
       for lIp, lPort, fIp, fPort in newConnections:
         newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort)
-        if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT:
+        newConnLine = newConnEntry.getLines()[0]
+        
+        if newConnLine.getType() != connEntry.Category.CIRCUIT:
           newEntries.append(newConnEntry)
+          
+          # updates exit port and client locale usage information
+          if newConnLine.isPrivate():
+            if newConnLine.getType() == connEntry.Category.INBOUND:
+              # client connection, update locale information
+              clientLocale = newConnLine.foreign.getLocale()
+              
+              if clientLocale:
+                self._clientLocaleUsage[clientLocale] = self._clientLocaleUsage.get(clientLocale, 0) + 1
+            elif newConnLine.getType() == connEntry.Category.EXIT:
+              exitPort = newConnLine.foreign.getPort()
+              self._exitPortUsage[exitPort] = self._exitPortUsage.get(exitPort, 0) + 1
       
       for circuitID in newCircuits:
         status, purpose, path = newCircuits[circuitID]
diff --git a/src/cli/connections/countPopup.py b/src/cli/connections/countPopup.py
new file mode 100644
index 0000000..341087f
--- /dev/null
+++ b/src/cli/connections/countPopup.py
@@ -0,0 +1,86 @@
+"""
+Provides a dialog with client locale or exiting port counts.
+"""
+
+import curses
+import operator
+
+import cli.controller
+import cli.popups
+
+from util import enum, log, uiTools
+
+CountType = enum.Enum("CLIENT_LOCALE", "EXIT_PORT")
+
+def showCountDialog(countType, counts):
+  """
+  Provides a dialog with bar graphs and percentages for the given set of
+  counts. Pressing any key closes the dialog.
+  
+  Arguments:
+    countType - type of counts being presented
+    counts    - mapping of labels to counts
+  """
+  
+  isNoStats = not counts
+  noStatsMsg = "Usage stats aren't available yet, press any key..."
+  
+  if isNoStats:
+    popup, width, height = cli.popups.init(3, len(noStatsMsg) + 4)
+  else:
+    popup, width, height = cli.popups.init(4 + max(1, len(counts)), 80)
+  if not popup: return
+  
+  try:
+    control = cli.controller.getController()
+    
+    popup.win.box()
+    
+    # dialog title
+    if countType == CountType.CLIENT_LOCALE:
+      title = "Client Locales"
+    elif countType == CountType.EXIT_PORT:
+      title = "Exiting Port Usage"
+    else:
+      title = ""
+      log.log(log.WARN, "Unrecognized count type: %s" % countType)
+    
+    popup.addstr(0, 0, title, curses.A_STANDOUT)
+    
+    if isNoStats:
+      popup.addstr(1, 2, noStatsMsg, curses.A_BOLD | uiTools.getColor("cyan"))
+    else:
+      sortedCounts = sorted(counts.iteritems(), key=operator.itemgetter(1))
+      sortedCounts.reverse()
+      
+      # constructs string formatting for the max key and value display width
+      keyWidth, valWidth, valueTotal = 3, 1, 0
+      for k, v in sortedCounts:
+        keyWidth = max(keyWidth, len(k))
+        valWidth = max(valWidth, len(str(v)))
+        valueTotal += v
+      
+      labelFormat = "%%-%is %%%ii (%%%%%%-2i)" % (keyWidth, valWidth)
+      
+      for i in range(height - 4):
+        k, v = sortedCounts[i]
+        label = labelFormat % (k, v, v * 100 / valueTotal)
+        popup.addstr(i + 1, 2, label, curses.A_BOLD | uiTools.getColor("green"))
+        
+        # All labels have the same size since they're based on the max widths.
+        # If this changes then this'll need to be the max label width.
+        labelWidth = len(label)
+        
+        # draws simple bar graph for percentages
+        fillWidth = v * (width - 4 - labelWidth) / valueTotal
+        for j in range(fillWidth):
+          popup.addstr(i + 1, 3 + labelWidth + j, " ", curses.A_STANDOUT | uiTools.getColor("red"))
+      
+      popup.addstr(height - 2, 2, "Press any key...")
+    
+    popup.win.refresh()
+    
+    curses.cbreak()
+    control.getScreen().getch()
+  finally: cli.popups.finalize()
+



More information about the tor-commits mailing list