[or-cvs] r23018: {arm} Revising externals definitions for torctl, and making a mirr (in arm: . dependencies dependencies/TorCtl release trunk trunk/src)

Damian Johnson atagar1 at gmail.com
Mon Aug 23 01:13:01 UTC 2010


Author: atagar
Date: 2010-08-23 01:13:01 +0000 (Mon, 23 Aug 2010)
New Revision: 23018

Added:
   arm/dependencies/
   arm/dependencies/TorCtl/
   arm/dependencies/TorCtl/GeoIPSupport.py
   arm/dependencies/TorCtl/PathSupport.py
   arm/dependencies/TorCtl/README
   arm/dependencies/TorCtl/SQLSupport.py
   arm/dependencies/TorCtl/ScanSupport.py
   arm/dependencies/TorCtl/StatsSupport.py
   arm/dependencies/TorCtl/TorCtl.py
   arm/dependencies/TorCtl/TorUtil.py
   arm/dependencies/TorCtl/__init__.py
   arm/dependencies/notes.txt
Removed:
   arm/trunk/src/TorCtl/
Modified:
   arm/release/
   arm/trunk/
   arm/trunk/src/
Log:
Revising externals definitions for torctl, and making a mirror based on the current git trunk.



Added: arm/dependencies/TorCtl/GeoIPSupport.py
===================================================================
--- arm/dependencies/TorCtl/GeoIPSupport.py	                        (rev 0)
+++ arm/dependencies/TorCtl/GeoIPSupport.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,140 @@
+#!/usr/bin/python
+
+import struct
+import socket
+import TorCtl
+import StatsSupport
+
+from TorUtil import plog
+try:
+  import GeoIP
+  # GeoIP data object: choose database here
+  geoip = GeoIP.new(GeoIP.GEOIP_STANDARD)
+  #geoip = GeoIP.open("./GeoLiteCity.dat", GeoIP.GEOIP_STANDARD)
+except:
+  plog("NOTICE", "No GeoIP library. GeoIPSupport.py will not work correctly")
+  # XXX: How do we bail entirely..  
+
+
+class Continent:
+  """ Continent class: The group attribute is to partition the continents
+      in groups, to determine the number of ocean crossings """
+  def __init__(self, continent_code):
+    self.code = continent_code 
+    self.group = None
+    self.countries = []
+
+  def contains(self, country_code):
+    return country_code in self.countries
+
+# Set countries to continents
+africa = Continent("AF")
+africa.group = 1
+africa.countries = ["AO","BF","BI","BJ","BV","BW","CD","CF","CG","CI","CM",
+   "CV","DJ","DZ","EG","EH","ER","ET","GA","GH","GM","GN","GQ","GW","HM","KE",
+   "KM","LR","LS","LY","MA","MG","ML","MR","MU","MW","MZ","NA","NE","NG","RE",
+   "RW","SC","SD","SH","SL","SN","SO","ST","SZ","TD","TF","TG","TN","TZ","UG",
+   "YT","ZA","ZM","ZR","ZW"]
+
+asia = Continent("AS")
+asia.group = 1
+asia.countries = ["AP","AE","AF","AM","AZ","BD","BH","BN","BT","CC","CN","CX",
+   "CY","GE","HK","ID","IL","IN","IO","IQ","IR","JO","JP","KG","KH","KP","KR",
+   "KW","KZ","LA","LB","LK","MM","MN","MO","MV","MY","NP","OM","PH","PK","PS",
+   "QA","RU","SA","SG","SY","TH","TJ","TM","TP","TR","TW","UZ","VN","YE"]
+
+europe = Continent("EU")
+europe.group = 1
+europe.countries = ["EU","AD","AL","AT","BA","BE","BG","BY","CH","CZ","DE",
+   "DK","EE","ES","FI","FO","FR","FX","GB","GI","GR","HR","HU","IE","IS","IT",
+   "LI","LT","LU","LV","MC","MD","MK","MT","NL","NO","PL","PT","RO","SE","SI",
+   "SJ","SK","SM","UA","VA","YU"]
+
+oceania = Continent("OC")
+oceania.group = 2
+oceania.countries = ["AS","AU","CK","FJ","FM","GU","KI","MH","MP","NC","NF",
+   "NR","NU","NZ","PF","PG","PN","PW","SB","TK","TO","TV","UM","VU","WF","WS"]
+
+north_america = Continent("NA")
+north_america.group = 0
+north_america.countries = ["CA","MX","US"]
+
+south_america = Continent("SA")
+south_america.group = 0
+south_america.countries = ["AG","AI","AN","AR","AW","BB","BM","BO","BR","BS",
+   "BZ","CL","CO","CR","CU","DM","DO","EC","FK","GD","GF","GL","GP","GS","GT",
+   "GY","HN","HT","JM","KN","KY","LC","MQ","MS","NI","PA","PE","PM","PR","PY",
+   "SA","SR","SV","TC","TT","UY","VC","VE","VG","VI"]
+
+# List of continents
+continents = [africa, asia, europe, north_america, oceania, south_america]
+
+def get_continent(country_code):
+  """ Perform country -- continent mapping """
+  for c in continents:
+    if c.contains(country_code):
+      return c
+  plog("INFO", country_code + " is not on any continent")
+  return None
+
+def get_country(ip):
+  """ Get the country via the library """
+  return geoip.country_code_by_addr(ip)
+
+def get_country_from_record(ip):
+  """ Get the country code out of a GeoLiteCity record (not used) """
+  record = geoip.record_by_addr(ip)
+  if record != None:
+    return record['country_code']
+
+class GeoIPRouter(TorCtl.Router):
+  # TODO: Its really shitty that this has to be a TorCtl.Router
+  # and can't be a StatsRouter..
+  """ Router class extended to GeoIP """
+  def __init__(self, router):
+    self.__dict__ = router.__dict__
+    self.country_code = get_country(self.get_ip_dotted())
+    if self.country_code != None: 
+      c = get_continent(self.country_code)
+      if c != None:
+        self.continent = c.code
+        self.cont_group = c.group
+    else: 
+      plog("INFO", self.nickname + ": Country code not found")
+      self.continent = None
+   
+  def get_ip_dotted(self):
+    """ Convert long int back to dotted quad string """
+    return socket.inet_ntoa(struct.pack('>I', self.ip))
+
+class GeoIPConfig:
+  """ Class to configure GeoIP-based path building """
+  def __init__(self, unique_countries=None, continent_crossings=4,
+     ocean_crossings=None, entry_country=None, middle_country=None,
+     exit_country=None, excludes=None):
+    # TODO: Somehow ensure validity of a configuration:
+    #   - continent_crossings >= ocean_crossings
+    #   - unique_countries=False --> continent_crossings!=None
+    #   - echelon? set entry_country to source and exit_country to None
+
+    # Do not use a country twice in a route 
+    # [True --> unique, False --> same or None --> pass] 
+    self.unique_countries = unique_countries
+    
+    # Configure max continent crossings in one path 
+    # [integer number 0-n or None --> ContinentJumper/UniqueContinent]
+    self.continent_crossings = continent_crossings
+    self.ocean_crossings = ocean_crossings
+ 
+    # Try to find an exit node in the destination country
+    # use exit_country as backup, if country cannot not be found
+    self.echelon = False
+
+    # Specify countries for positions [single country code or None]
+    self.entry_country = entry_country
+    self.middle_country = middle_country
+    self.exit_country = exit_country
+
+    # List of countries not to use in routes 
+    # [(empty) list of country codes or None]
+    self.excludes = excludes

Added: arm/dependencies/TorCtl/PathSupport.py
===================================================================
--- arm/dependencies/TorCtl/PathSupport.py	                        (rev 0)
+++ arm/dependencies/TorCtl/PathSupport.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,2108 @@
+#!/usr/bin/python
+"""
+
+Support classes for path construction
+
+The PathSupport package builds on top of TorCtl.TorCtl. It provides a
+number of interfaces that make path construction easier.
+
+The inheritance diagram for event handling is as follows:
+TorCtl.EventHandler <- TorCtl.ConsensusTracker <- PathBuilder 
+  <- CircuitHandler <- StreamHandler.
+
+Basically, EventHandler is what gets all the control port events
+packaged in nice clean classes (see help(TorCtl) for information on
+those). 
+
+ConsensusTracker tracks the NEWCONSENSUS and NEWDESC events to maintain
+a view of the network that is consistent with the Tor client's current
+consensus.
+
+PathBuilder inherits from ConsensusTracker and is what builds all
+circuits based on the requirements specified in the SelectionManager
+instance passed to its constructor. It also handles attaching streams to
+circuits. It only handles one building one circuit at a time.
+
+CircuitHandler optionally inherits from PathBuilder, and overrides its
+circuit event handling to manage building a pool of circuits as opposed
+to just one. It still uses the SelectionManager for path selection.
+
+StreamHandler inherits from CircuitHandler, and is what governs the
+attachment of an incoming stream on to one of the multiple circuits of
+the circuit handler. 
+
+The SelectionManager is essentially a configuration wrapper around the
+most elegant portions of TorFlow: NodeGenerators, NodeRestrictions, and
+PathRestrictions. It extends from a BaseSelectionManager that provides
+a basic example of using these mechanisms for custom implementations.
+
+In the SelectionManager, a NodeGenerator is used to choose the nodes
+probabilistically according to some distribution while obeying the
+NodeRestrictions. These generators (one per hop) are handed off to the
+PathSelector, which uses the generators to build a complete path that
+satisfies the PathRestriction requirements.
+
+Have a look at the class hierarchy directly below to get a feel for how
+the restrictions fit together, and what options are available.
+
+"""
+
+import TorCtl
+import re
+import struct
+import random
+import socket
+import copy
+import Queue
+import time
+import TorUtil
+import traceback
+import threading
+from TorUtil import *
+
+import sys
+if sys.version_info < (2, 5):
+  from sets import Set as set
+
+__all__ = ["NodeRestrictionList", "PathRestrictionList",
+"PercentileRestriction", "OSRestriction", "ConserveExitsRestriction",
+"FlagsRestriction", "MinBWRestriction", "VersionIncludeRestriction",
+"VersionExcludeRestriction", "VersionRangeRestriction",
+"ExitPolicyRestriction", "NodeRestriction", "PathRestriction",
+"OrNodeRestriction", "MetaNodeRestriction", "AtLeastNNodeRestriction",
+"NotNodeRestriction", "Subnet16Restriction", "UniqueRestriction",
+"NodeGenerator", "UniformGenerator", "OrderedExitGenerator",
+"BwWeightedGenerator", "PathSelector", "Connection", "NickRestriction",
+"IdHexRestriction", "PathBuilder", "CircuitHandler", "StreamHandler",
+"SelectionManager", "BaseSelectionManager", "CountryCodeRestriction",
+"CountryRestriction", "UniqueCountryRestriction", "SingleCountryRestriction",
+"ContinentRestriction", "ContinentJumperRestriction",
+"UniqueContinentRestriction", "MetaPathRestriction", "RateLimitedRestriction",
+"SmartSocket"]
+
+#################### Path Support Interfaces #####################
+
+class RestrictionError(Exception):
+  "Error raised for issues with applying restrictions"
+  pass
+
+class NoNodesRemain(RestrictionError):
+  "Error raised for issues with applying restrictions"
+  pass
+
+class NodeRestriction:
+  "Interface for node restriction policies"
+  def r_is_ok(self, r):
+    "Returns true if Router 'r' is acceptable for this restriction"
+    return True  
+
+class PathRestriction:
+  "Interface for path restriction policies"
+  def path_is_ok(self, path):
+    "Return true if the list of Routers in path satisfies this restriction"
+    return True  
+
+# TODO: Or, Not, N of M
+class MetaPathRestriction(PathRestriction):
+  "MetaPathRestrictions are path restriction aggregators."
+  def add_restriction(self, rstr): raise NotImplemented()
+  def del_restriction(self, RestrictionClass): raise NotImplemented()
+ 
+class PathRestrictionList(MetaPathRestriction):
+  """Class to manage a list of PathRestrictions"""
+  def __init__(self, restrictions):
+    "Constructor. 'restrictions' is a list of PathRestriction instances"
+    self.restrictions = restrictions
+  
+  def path_is_ok(self, path):
+    "Given list if Routers in 'path', check it against each restriction."
+    for rs in self.restrictions:
+      if not rs.path_is_ok(path):
+        return False
+    return True
+
+  def add_restriction(self, rstr):
+    "Add a PathRestriction 'rstr' to the list"
+    self.restrictions.append(rstr)
+
+  def del_restriction(self, RestrictionClass):
+    "Remove all PathRestrictions of type RestrictionClass from the list."
+    self.restrictions = filter(
+        lambda r: not isinstance(r, RestrictionClass),
+          self.restrictions)
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(map(str, self.restrictions))+")"
+
+class NodeGenerator:
+  "Interface for node generation"
+  def __init__(self, sorted_r, rstr_list):
+    """Constructor. Takes a bandwidth-sorted list of Routers 'sorted_r' 
+    and a NodeRestrictionList 'rstr_list'"""
+    self.rstr_list = rstr_list
+    self.rebuild(sorted_r)
+
+  def reset_restriction(self, rstr_list):
+    "Reset the restriction list to a new list"
+    self.rstr_list = rstr_list
+    self.rebuild()
+
+  def rewind(self):
+    "Rewind the generator to the 'beginning'"
+    self.routers = copy.copy(self.rstr_routers)
+    if not self.routers:
+      plog("NOTICE", "No routers left after restrictions applied: "+str(self.rstr_list))
+      raise NoNodesRemain(str(self.rstr_list))
+ 
+  def rebuild(self, sorted_r=None):
+    """ Extra step to be performed when new routers are added or when
+    the restrictions change. """
+    if sorted_r != None:
+      self.sorted_r = sorted_r
+    self.rstr_routers = filter(lambda r: self.rstr_list.r_is_ok(r), self.sorted_r)
+    if not self.rstr_routers:
+      plog("NOTICE", "No routers left after restrictions applied: "+str(self.rstr_list))
+      raise NoNodesRemain(str(self.rstr_list))
+
+  def mark_chosen(self, r):
+    """Mark a router as chosen: remove it from the list of routers 
+     that can be returned in the future"""
+    self.routers.remove(r)
+
+  def all_chosen(self):
+    "Return true if all the routers have been marked as chosen"
+    return not self.routers
+
+  def generate(self):
+    "Return a python generator that yields routers according to the policy"
+    raise NotImplemented()
+
+class Connection(TorCtl.Connection):
+  """Extended Connection class that provides a method for building circuits"""
+  def __init__(self, sock):
+    TorCtl.Connection.__init__(self,sock)
+  def build_circuit(self, path):
+    "Tell Tor to build a circuit chosen by the PathSelector 'path_sel'"
+    circ = Circuit()
+    circ.path = path
+    circ.exit = circ.path[len(path)-1]
+    circ.circ_id = self.extend_circuit(0, circ.id_path())
+    return circ
+
+######################## Node Restrictions ########################
+
+# TODO: We still need more path support implementations
+#  - NodeRestrictions:
+#    - Uptime/LongLivedPorts (Does/should hibernation count?)
+#    - Published/Updated
+#    - Add a /8 restriction for ExitPolicy?
+#  - PathRestrictions:
+#    - NodeFamily
+#    - GeoIP:
+#      - Mathematical/empirical study of predecessor expectation
+#        - If middle node on the same continent as exit, exit learns nothing
+#        - else, exit has a bias on the continent of origin of user
+#          - Language and browser accept string determine this anyway
+#      - ContinentRestrictor (avoids doing more than N continent crossings)
+#      - EchelonPhobicRestrictor
+#        - Does not cross international boundaries for client->Entry or
+#          Exit->destination hops
+
+class PercentileRestriction(NodeRestriction):
+  """Restriction to cut out a percentile slice of the network."""
+  def __init__(self, pct_skip, pct_fast, r_list):
+    """Constructor. Sets up the restriction such that routers in the 
+     'pct_skip' to 'pct_fast' percentile of bandwidth rankings are 
+     returned from the sorted list 'r_list'"""
+    self.pct_fast = pct_fast
+    self.pct_skip = pct_skip
+    self.sorted_r = r_list
+
+  def r_is_ok(self, r):
+    "Returns true if r is in the percentile boundaries (by rank)"
+    if r.list_rank < len(self.sorted_r)*self.pct_skip/100: return False
+    elif r.list_rank > len(self.sorted_r)*self.pct_fast/100: return False
+    
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.pct_skip)+","+str(self.pct_fast)+")"
+
+class UptimeRestriction(NodeRestriction):
+  """Restriction to filter out routers with uptimes < min_uptime or
+     > max_uptime"""
+  def __init__(self, min_uptime=None, max_uptime=None):
+    self.min_uptime = min_uptime
+    self.max_uptime = max_uptime
+
+  def r_is_ok(self, r):
+    "Returns true if r is in the uptime boundaries"
+    if self.min_uptime and r.uptime < self.min_uptime: return False
+    if self.max_uptime and r.uptime > self.max_uptime: return False
+    return True
+
+class RankRestriction(NodeRestriction):
+  """Restriction to cut out a list-rank slice of the network."""
+  def __init__(self, rank_skip, rank_stop):
+    self.rank_skip = rank_skip
+    self.rank_stop = rank_stop
+
+  def r_is_ok(self, r):
+    "Returns true if r is in the boundaries (by rank)"
+    if r.list_rank < self.rank_skip: return False
+    elif r.list_rank > self.rank_stop: return False
+    
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.rank_skip)+","+str(self.rank_stop)+")"
+    
+class OSRestriction(NodeRestriction):
+  "Restriction based on operating system"
+  def __init__(self, ok, bad=[]):
+    """Constructor. Accept router OSes that match regexes in 'ok', 
+       rejects those that match regexes in 'bad'."""
+    self.ok = ok
+    self.bad = bad
+
+  def r_is_ok(self, r):
+    "Returns true if r is in 'ok', false if 'r' is in 'bad'. If 'ok'"
+    for y in self.ok:
+      if re.search(y, r.os):
+        return True
+    for b in self.bad:
+      if re.search(b, r.os):
+        return False
+    if self.ok: return False
+    if self.bad: return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.ok)+","+str(self.bad)+")"
+
+class ConserveExitsRestriction(NodeRestriction):
+  "Restriction to reject exits from selection"
+  def __init__(self, exit_ports=None):
+    self.exit_ports = exit_ports
+
+  def r_is_ok(self, r):
+    if self.exit_ports:
+      for port in self.exit_ports:
+        if r.will_exit_to("255.255.255.255", port):
+          return False
+      return True
+    return not "Exit" in r.flags
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+class FlagsRestriction(NodeRestriction):
+  "Restriction for mandatory and forbidden router flags"
+  def __init__(self, mandatory, forbidden=[]):
+    """Constructor. 'mandatory' and 'forbidden' are both lists of router 
+     flags as strings."""
+    self.mandatory = mandatory
+    self.forbidden = forbidden
+
+  def r_is_ok(self, router):
+    for m in self.mandatory:
+      if not m in router.flags: return False
+    for f in self.forbidden:
+      if f in router.flags: return False
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.mandatory)+","+str(self.forbidden)+")"
+
+class NickRestriction(NodeRestriction):
+  """Require that the node nickname is as specified"""
+  def __init__(self, nickname):
+    self.nickname = nickname
+
+  def r_is_ok(self, router):
+    return router.nickname == self.nickname
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.nickname)+")"
+
+class IdHexRestriction(NodeRestriction):
+  """Require that the node idhash is as specified"""
+  def __init__(self, idhex):
+    if idhex[0] == '$':
+      self.idhex = idhex[1:].upper()
+    else:
+      self.idhex = idhex.upper()
+
+  def r_is_ok(self, router):
+    return router.idhex == self.idhex
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.idhex)+")"
+ 
+class MinBWRestriction(NodeRestriction):
+  """Require a minimum bandwidth"""
+  def __init__(self, minbw):
+    self.min_bw = minbw
+
+  def r_is_ok(self, router): return router.bw >= self.min_bw
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.min_bw)+")"
+
+class RateLimitedRestriction(NodeRestriction):
+  def __init__(self, limited=True):
+    self.limited = limited
+
+  def r_is_ok(self, router): return router.rate_limited == self.limited
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.limited)+")"
+   
+class VersionIncludeRestriction(NodeRestriction):
+  """Require that the version match one in the list"""
+  def __init__(self, eq):
+    "Constructor. 'eq' is a list of versions as strings"
+    self.eq = map(TorCtl.RouterVersion, eq)
+  
+  def r_is_ok(self, router):
+    """Returns true if the version of 'router' matches one of the 
+     specified versions."""
+    for e in self.eq:
+      if e == router.version:
+        return True
+    return False
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.eq)+")"
+
+class VersionExcludeRestriction(NodeRestriction):
+  """Require that the version not match one in the list"""
+  def __init__(self, exclude):
+    "Constructor. 'exclude' is a list of versions as strings"
+    self.exclude = map(TorCtl.RouterVersion, exclude)
+  
+  def r_is_ok(self, router):
+    """Returns false if the version of 'router' matches one of the 
+     specified versions."""
+    for e in self.exclude:
+      if e == router.version:
+        return False
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(map(str, self.exclude))+")"
+
+class VersionRangeRestriction(NodeRestriction):
+  """Require that the versions be inside a specified range""" 
+  def __init__(self, gr_eq, less_eq=None):
+    self.gr_eq = TorCtl.RouterVersion(gr_eq)
+    if less_eq: self.less_eq = TorCtl.RouterVersion(less_eq)
+    else: self.less_eq = None
+  
+  def r_is_ok(self, router):
+    return (not self.gr_eq or router.version >= self.gr_eq) and \
+        (not self.less_eq or router.version <= self.less_eq)
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.gr_eq)+","+str(self.less_eq)+")"
+
+class ExitPolicyRestriction(NodeRestriction):
+  """Require that a router exit to an ip+port"""
+  def __init__(self, to_ip, to_port):
+    self.to_ip = to_ip
+    self.to_port = to_port
+
+  def r_is_ok(self, r): return r.will_exit_to(self.to_ip, self.to_port)
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.to_ip)+","+str(self.to_port)+")"
+
+class MetaNodeRestriction(NodeRestriction):
+  """Interface for a NodeRestriction that is an expression consisting of 
+     multiple other NodeRestrictions"""
+  def add_restriction(self, rstr): raise NotImplemented()
+  # TODO: these should collapse the restriction and return a new
+  # instance for re-insertion (or None)
+  def next_rstr(self): raise NotImplemented()
+  def del_restriction(self, RestrictionClass): raise NotImplemented()
+
+class OrNodeRestriction(MetaNodeRestriction):
+  """MetaNodeRestriction that is the boolean or of two or more
+     NodeRestrictions"""
+  def __init__(self, rs):
+    "Constructor. 'rs' is a list of NodeRestrictions"
+    self.rstrs = rs
+
+  def r_is_ok(self, r):
+    "Returns true if one of 'rs' is true for this router"
+    for rs in self.rstrs:
+      if rs.r_is_ok(r):
+        return True
+    return False
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(map(str, self.rstrs))+")"
+
+class NotNodeRestriction(MetaNodeRestriction):
+  """Negates a single restriction"""
+  def __init__(self, a):
+    self.a = a
+
+  def r_is_ok(self, r): return not self.a.r_is_ok(r)
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.a)+")"
+
+class AtLeastNNodeRestriction(MetaNodeRestriction):
+  """MetaNodeRestriction that is true if at least n member 
+     restrictions are true."""
+  def __init__(self, rstrs, n):
+    self.rstrs = rstrs
+    self.n = n
+
+  def r_is_ok(self, r):
+    cnt = 0
+    for rs in self.rstrs:
+      if rs.r_is_ok(r):
+        cnt += 1
+    if cnt < self.n: return False
+    else: return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(map(str, self.rstrs))+","+str(self.n)+")"
+
+class NodeRestrictionList(MetaNodeRestriction):
+  "Class to manage a list of NodeRestrictions"
+  def __init__(self, restrictions):
+    "Constructor. 'restrictions' is a list of NodeRestriction instances"
+    self.restrictions = restrictions
+
+  def r_is_ok(self, r):
+    "Returns true of Router 'r' passes all of the contained restrictions"
+    for rs in self.restrictions:
+      if not rs.r_is_ok(r): return False
+    return True
+
+  def add_restriction(self, restr):
+    "Add a NodeRestriction 'restr' to the list of restrictions"
+    self.restrictions.append(restr)
+
+  # TODO: This does not collapse meta restrictions..
+  def del_restriction(self, RestrictionClass):
+    """Remove all restrictions of type RestrictionClass from the list.
+       Does NOT inspect or collapse MetaNode Restrictions (though 
+       MetaRestrictions can be removed if RestrictionClass is 
+       MetaNodeRestriction)"""
+    self.restrictions = filter(
+        lambda r: not isinstance(r, RestrictionClass),
+          self.restrictions)
+  
+  def clear(self):
+    """ Remove all restrictions """
+    self.restrictions = []
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(map(str, self.restrictions))+")"
+
+
+#################### Path Restrictions #####################
+
+class Subnet16Restriction(PathRestriction):
+  """PathRestriction that mandates that no two nodes from the same 
+     /16 subnet be in the path"""
+  def path_is_ok(self, path):
+    mask16 = struct.unpack(">I", socket.inet_aton("255.255.0.0"))[0]
+    ip16 = path[0].ip & mask16
+    for r in path[1:]:
+      if ip16 == (r.ip & mask16):
+        return False
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+class UniqueRestriction(PathRestriction):
+  """Path restriction that mandates that the same router can't appear more
+     than once in a path"""
+  def path_is_ok(self, path):
+    for i in xrange(0,len(path)):
+      if path[i] in path[:i]:
+        return False
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+#################### GeoIP Restrictions ###################
+
+class CountryCodeRestriction(NodeRestriction):
+  """ Ensure that the country_code is set """
+  def r_is_ok(self, r):
+    return r.country_code != None
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+class CountryRestriction(NodeRestriction):
+  """ Only accept nodes that are in 'country_code' """
+  def __init__(self, country_code):
+    self.country_code = country_code
+
+  def r_is_ok(self, r):
+    return r.country_code == self.country_code
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.country_code)+")"
+
+class ExcludeCountriesRestriction(NodeRestriction):
+  """ Exclude a list of countries """
+  def __init__(self, countries):
+    self.countries = countries
+
+  def r_is_ok(self, r):
+    return not (r.country_code in self.countries)
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.countries)+")"
+
+class UniqueCountryRestriction(PathRestriction):
+  """ Ensure every router to have a distinct country_code """
+  def path_is_ok(self, path):
+    for i in xrange(0, len(path)-1):
+      for j in xrange(i+1, len(path)):
+        if path[i].country_code == path[j].country_code:
+          return False;
+    return True;
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+class SingleCountryRestriction(PathRestriction):
+  """ Ensure every router to have the same country_code """
+  def path_is_ok(self, path):
+    country_code = path[0].country_code
+    for r in path:
+      if country_code != r.country_code:
+        return False
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+class ContinentRestriction(PathRestriction):
+  """ Do not more than n continent crossings """
+  # TODO: Add src and dest
+  def __init__(self, n, src=None, dest=None):
+    self.n = n
+
+  def path_is_ok(self, path):
+    crossings = 0
+    prev = None
+    # Compute crossings until now
+    for r in path:
+      # Jump over the first router
+      if prev:
+        if r.continent != prev.continent:
+          crossings += 1
+      prev = r
+    if crossings > self.n: return False
+    else: return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.n)+")"
+
+class ContinentJumperRestriction(PathRestriction):
+  """ Ensure continent crossings between all hops """
+  def path_is_ok(self, path):
+    prev = None
+    for r in path:
+      # Jump over the first router
+      if prev:
+        if r.continent == prev.continent:
+          return False
+      prev = r
+    return True
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+class UniqueContinentRestriction(PathRestriction):
+  """ Ensure every hop to be on a different continent """
+  def path_is_ok(self, path):
+    for i in xrange(0, len(path)-1):
+      for j in xrange(i+1, len(path)):
+        if path[i].continent == path[j].continent:
+          return False;
+    return True;
+
+  def __str__(self):
+    return self.__class__.__name__+"()"
+
+class OceanPhobicRestriction(PathRestriction):
+  """ Not more than n ocean crossings """
+  # TODO: Add src and dest
+  def __init__(self, n, src=None, dest=None):
+    self.n = n
+
+  def path_is_ok(self, path):
+    crossings = 0
+    prev = None
+    # Compute ocean crossings until now
+    for r in path:
+      # Jump over the first router
+      if prev:
+        if r.cont_group != prev.cont_group:
+          crossings += 1
+      prev = r
+    if crossings > self.n: return False
+    else: return True
+
+  def __str__(self):
+    return self.__class__.__name__+"("+str(self.n)+")"
+
+#################### Node Generators ######################
+
+class UniformGenerator(NodeGenerator):
+  """NodeGenerator that produces nodes in the uniform distribution"""
+  def generate(self):
+    # XXX: hrmm.. this is not really the right thing to check
+    while not self.all_chosen():
+      yield random.choice(self.routers)
+     
+class ExactUniformGenerator(NodeGenerator):
+  """NodeGenerator that produces nodes randomly, yet strictly uniformly 
+     over time"""
+  def __init__(self, sorted_r, rstr_list, position=0):
+    self.position = position
+    NodeGenerator.__init__(self, sorted_r, rstr_list)  
+
+  def generate(self):
+    min_gen = min(map(lambda r: r._generated[self.position], self.routers))
+    choices = filter(lambda r: r._generated[self.position]==min_gen, 
+                       self.routers)
+    while choices:
+      r = random.choice(choices)
+      yield r
+      choices.remove(r)
+
+    choices = filter(lambda r: r._generated[self.position]==min_gen,
+                       self.routers)
+    plog("NOTICE", "Ran out of choices in ExactUniformGenerator. Incrementing nodes")
+    for r in choices:
+      r._generated[self.position] += 1
+
+  def mark_chosen(self, r):
+    r._generated[self.position] += 1
+    NodeGenerator.mark_chosen(self, r)
+
+  def rebuild(self, sorted_r=None):
+    plog("DEBUG", "Rebuilding ExactUniformGenerator")
+    NodeGenerator.rebuild(self, sorted_r)
+    for r in self.rstr_routers:
+      lgen = len(r._generated)
+      if lgen < self.position+1:
+        for i in xrange(lgen, self.position+1):
+          r._generated.append(0)
+
+
+class OrderedExitGenerator(NodeGenerator):
+  """NodeGenerator that produces exits in an ordered fashion for a 
+     specific port"""
+  def __init__(self, to_port, sorted_r, rstr_list):
+    self.to_port = to_port
+    self.next_exit_by_port = {}
+    NodeGenerator.__init__(self, sorted_r, rstr_list)
+
+  def rewind(self):
+    NodeGenerator.rewind(self)
+    if self.to_port not in self.next_exit_by_port or not self.next_exit_by_port[self.to_port]:
+      self.next_exit_by_port[self.to_port] = 0
+      self.last_idx = len(self.routers)
+    else:
+      self.last_idx = self.next_exit_by_port[self.to_port]
+
+  def set_port(self, port):
+    self.to_port = port
+    self.rewind()
+     
+  def mark_chosen(self, r):
+    self.next_exit_by_port[self.to_port] += 1
+  
+  def all_chosen(self):
+    return self.last_idx == self.next_exit_by_port[self.to_port]
+
+  def generate(self):
+    while True: # A do..while would be real nice here..
+      if self.next_exit_by_port[self.to_port] >= len(self.routers):
+        self.next_exit_by_port[self.to_port] = 0
+      yield self.routers[self.next_exit_by_port[self.to_port]]
+      self.next_exit_by_port[self.to_port] += 1
+      if self.last_idx == self.next_exit_by_port[self.to_port]:
+        break
+
+class BwWeightedGenerator(NodeGenerator):
+  """
+
+  This is a generator designed to match the Tor Path Selection
+  algorithm.  It will generate nodes weighted by their bandwidth,
+  but take the appropriate weighting into account against guard
+  nodes and exit nodes when they are chosen for positions other
+  than guard/exit. For background see:
+  routerlist.c::smartlist_choose_by_bandwidth(),
+  http://archives.seul.org/or/dev/Jul-2007/msg00021.html,
+  http://archives.seul.org/or/dev/Jul-2007/msg00056.html, and
+  https://tor-svn.freehaven.net/svn/tor/trunk/doc/spec/path-spec.txt
+  The formulas used are from the first or-dev link, but are proven
+  optimal and equivalent to the ones now used in routerlist.c in the 
+  second or-dev link.
+  
+  """ 
+  def __init__(self, sorted_r, rstr_list, pathlen, exit=False, guard=False):
+    """ Pass exit=True to create a generator for exit-nodes """
+    self.max_bandwidth = 10000000
+    # Out for an exit-node?
+    self.exit = exit
+    # Is this a guard node? 
+    self.guard = guard
+    # Different sums of bandwidths
+    self.total_bw = 0
+    self.total_exit_bw = 0
+    self.total_guard_bw = 0
+    self.total_weighted_bw = 0
+    self.pathlen = pathlen
+    NodeGenerator.__init__(self, sorted_r, rstr_list)
+
+  def rebuild(self, sorted_r=None):
+    NodeGenerator.rebuild(self, sorted_r)
+    NodeGenerator.rewind(self)
+    # Set the exit_weight
+    # We are choosing a non-exit
+    self.total_exit_bw = 0
+    self.total_guard_bw = 0
+    self.total_bw = 0
+    for r in self.routers:
+      # TODO: Check max_bandwidth and cap...
+      self.total_bw += r.bw
+      if "Exit" in r.flags:
+        self.total_exit_bw += r.bw
+      if "Guard" in r.flags:
+        self.total_guard_bw += r.bw
+
+    bw_per_hop = (1.0*self.total_bw)/self.pathlen
+
+    # Print some debugging info about bandwidth ratios
+    if self.total_bw > 0:
+      e_ratio = self.total_exit_bw/float(self.total_bw)
+      g_ratio = self.total_guard_bw/float(self.total_bw)
+    else:
+      g_ratio = 0
+      e_ratio = 0
+    plog("DEBUG",
+       "E = " + str(self.total_exit_bw) +
+       ", G = " + str(self.total_guard_bw) +
+       ", T = " + str(self.total_bw) +
+       ", g_ratio = " + str(g_ratio) + ", e_ratio = " +str(e_ratio) +
+       ", bw_per_hop = " + str(bw_per_hop))
+   
+    if self.exit:
+      self.exit_weight = 1.0
+    else:
+      if self.total_exit_bw < bw_per_hop:
+        # Don't use exit nodes at all
+        self.exit_weight = 0
+      else:
+        if self.total_exit_bw > 0:
+          self.exit_weight = ((self.total_exit_bw-bw_per_hop)/self.total_exit_bw)
+        else: self.exit_weight = 0
+
+    if self.guard:
+      self.guard_weight = 1.0
+    else:
+      if self.total_guard_bw < bw_per_hop:
+        # Don't use exit nodes at all
+        self.guard_weight = 0
+      else:
+        if self.total_guard_bw > 0:
+          self.guard_weight = ((self.total_guard_bw-bw_per_hop)/self.total_guard_bw)
+        else: self.guard_weight = 0
+    
+    for r in self.routers:
+      bw = r.bw
+      if "Exit" in r.flags:
+        bw *= self.exit_weight
+      if "Guard" in r.flags:
+        bw *= self.guard_weight
+      self.total_weighted_bw += bw
+
+    self.total_weighted_bw = int(self.total_weighted_bw)
+    plog("DEBUG", "Bw: "+str(self.total_weighted_bw)+"/"+str(self.total_bw)
+          +". The exit-weight is: "+str(self.exit_weight)
+          + ", guard weight is: "+str(self.guard_weight))
+
+  def generate(self):
+    while True:
+      # Choose a suitable random int
+      i = random.randint(0, self.total_weighted_bw)
+
+      # Go through the routers
+      for r in self.routers:
+        # Below zero here means next() -> choose a new random int+router 
+        if i < 0: break
+        bw = r.bw
+        if "Exit" in r.flags:
+          bw *= self.exit_weight
+        if "Guard" in r.flags:
+          bw *= self.guard_weight
+
+        i -= bw
+        if i < 0:
+          plog("DEBUG", "Chosen router with a bandwidth of: " + str(r.bw))
+          yield r
+
+####################### Secret Sauce ###########################
+
+class PathError(Exception):
+  pass
+
+class NoRouters(PathError):
+  pass
+
+class PathSelector:
+  """Implementation of path selection policies. Builds a path according
+     to entry, middle, and exit generators that satisfies the path 
+     restrictions."""
+  def __init__(self, entry_gen, mid_gen, exit_gen, path_restrict):
+    """Constructor. The first three arguments are NodeGenerators with 
+     their appropriate restrictions. The 'path_restrict' is a
+     PathRestrictionList"""
+    self.entry_gen = entry_gen
+    self.mid_gen = mid_gen
+    self.exit_gen = exit_gen
+    self.path_restrict = path_restrict
+
+  def rebuild_gens(self, sorted_r):
+    "Rebuild the 3 generators with a new sorted router list"
+    self.entry_gen.rebuild(sorted_r)
+    self.mid_gen.rebuild(sorted_r)
+    self.exit_gen.rebuild(sorted_r)
+
+  def select_path(self, pathlen):
+    """Creates a path of 'pathlen' hops, and returns it as a list of
+       Router instances"""
+    self.entry_gen.rewind()
+    self.mid_gen.rewind()
+    self.exit_gen.rewind()
+    entry = self.entry_gen.generate()
+    mid = self.mid_gen.generate()
+    ext = self.exit_gen.generate()
+      
+    plog("DEBUG", "Selecting path..")
+
+    while True:
+      path = []
+      plog("DEBUG", "Building path..")
+      try:
+        if pathlen == 1:
+          path = [ext.next()]
+        else:
+          path.append(entry.next())
+          for i in xrange(1, pathlen-1):
+            path.append(mid.next())
+          path.append(ext.next())
+        if self.path_restrict.path_is_ok(path):
+          self.entry_gen.mark_chosen(path[0])
+          for i in xrange(1, pathlen-1):
+            self.mid_gen.mark_chosen(path[i])
+          self.exit_gen.mark_chosen(path[pathlen-1])
+          plog("DEBUG", "Marked path.")
+          break
+        else:
+          plog("DEBUG", "Path rejected by path restrictions.")
+      except StopIteration:
+        plog("NOTICE", "Ran out of routers during buildpath..");
+        self.entry_gen.rewind()
+        self.mid_gen.rewind()
+        self.exit_gen.rewind()
+        entry = self.entry_gen.generate()
+        mid = self.mid_gen.generate()
+        ext = self.exit_gen.generate()
+    for r in path:
+      r.refcount += 1
+      plog("DEBUG", "Circ refcount "+str(r.refcount)+" for "+r.idhex)
+    return path
+
+# TODO: Implement example manager.
+class BaseSelectionManager:
+   """
+   The BaseSelectionManager is a minimalistic node selection manager.
+
+   It is meant to be used with a PathSelector that consists of an
+   entry NodeGenerator, a middle NodeGenerator, and an exit NodeGenerator.
+
+   However, none of these are absolutely necessary. It is possible
+   to completely avoid them if you wish by hacking whatever selection
+   mechanisms you want straight into this interface and then passing
+   an instance to a PathBuilder implementation.
+   """
+   def __init__(self):
+     self.bad_restrictions = False
+     self.consensus = None
+
+   def reconfigure(self, consensus=None):
+     """ 
+     This method is called whenever a significant configuration change
+     occurs. Currently, this only happens via PathBuilder.__init__ and
+     PathBuilder.schedule_selmgr().
+     
+     This method should NOT throw any exceptions.
+     """
+     pass
+
+   def new_consensus(self, consensus):
+     """ 
+     This method is called whenever a consensus change occurs.
+     
+     This method should NOT throw any exceptions.
+     """
+     pass
+
+   def set_exit(self, exit_name):
+     """
+     This method provides notification that a fixed exit is desired.
+
+     This method should NOT throw any exceptions.
+     """
+     pass
+
+   def set_target(self, host, port):
+     """
+     This method provides notification that a new target endpoint is
+     desired.
+
+     May throw a RestrictionError if target is impossible to reach.
+     """
+     pass
+
+   def select_path(self):
+     """
+     Returns a new path in the form of a list() of Router instances.
+
+     May throw a RestrictionError.
+     """
+     pass
+
+class SelectionManager(BaseSelectionManager):
+  """Helper class to handle configuration updates
+    
+    The methods are NOT threadsafe. They may ONLY be called from
+    EventHandler's thread. This means that to update the selection 
+    manager, you must schedule a config update job using 
+    PathBuilder.schedule_selmgr() with a worker function to modify 
+    this object.
+ 
+    XXX: Warning. The constructor of this class is subject to change
+    and may undergo reorganization in the near future. Watch for falling 
+    bits.
+    """
+  # XXX: Hrmm, consider simplifying this. It is confusing and unweildy.
+  def __init__(self, pathlen, order_exits,
+         percent_fast, percent_skip, min_bw, use_all_exits,
+         uniform, use_exit, use_guards,geoip_config=None,
+         restrict_guards=False, extra_node_rstr=None, exit_ports=None):
+    BaseSelectionManager.__init__(self)
+    self.__ordered_exit_gen = None 
+    self.pathlen = pathlen
+    self.order_exits = order_exits
+    self.percent_fast = percent_fast
+    self.percent_skip = percent_skip
+    self.min_bw = min_bw
+    self.use_all_exits = use_all_exits
+    self.uniform = uniform
+    self.exit_id = use_exit
+    self.use_guards = use_guards
+    self.geoip_config = geoip_config
+    self.restrict_guards_only = restrict_guards
+    self.bad_restrictions = False
+    self.consensus = None
+    self.exit_ports = exit_ports
+    self.extra_node_rstr=extra_node_rstr
+
+  def reconfigure(self, consensus=None):
+    try:
+      self._reconfigure(consensus)
+      self.bad_restrictions = False
+    except NoNodesRemain:
+      plog("WARN", "No nodes remain in selection manager")
+      self.bad_restrictions = True
+    return self.bad_restrictions
+
+  def _reconfigure(self, consensus=None):
+    """This function is called after a configuration change, 
+     to rebuild the RestrictionLists."""
+    if consensus: 
+      plog("DEBUG", "Reconfigure with consensus")
+      self.consensus = consensus
+    else:
+      plog("DEBUG", "Reconfigure without consensus")
+
+    sorted_r = self.consensus.sorted_r
+
+    if self.use_all_exits:
+      self.path_rstr = PathRestrictionList([UniqueRestriction()])
+    else:
+      self.path_rstr = PathRestrictionList(
+           [Subnet16Restriction(), UniqueRestriction()])
+  
+    if self.use_guards: entry_flags = ["Guard", "Running", "Fast"]
+    else: entry_flags = ["Running", "Fast"]
+
+    if self.restrict_guards_only:
+      nonentry_skip = 0
+      nonentry_fast = 100
+    else:
+      nonentry_skip = self.percent_skip
+      nonentry_fast = self.percent_fast
+
+    # XXX: sometimes we want the ability to do uniform scans
+    # without the conserve exit restrictions..
+    entry_rstr = NodeRestrictionList(
+      [PercentileRestriction(self.percent_skip, self.percent_fast, sorted_r),
+       OrNodeRestriction(
+           [FlagsRestriction(["BadExit"]),
+           ConserveExitsRestriction(self.exit_ports)]),
+       FlagsRestriction(entry_flags, [])]
+    )
+    mid_rstr = NodeRestrictionList(
+      [PercentileRestriction(nonentry_skip, nonentry_fast, sorted_r),
+       OrNodeRestriction(
+           [FlagsRestriction(["BadExit"]),
+           ConserveExitsRestriction(self.exit_ports)]),
+       FlagsRestriction(["Running","Fast"], [])]
+
+    )
+
+    if self.exit_id:
+      self._set_exit(self.exit_id)
+      plog("DEBUG", "Applying Setexit: "+self.exit_id)
+      self.exit_rstr = NodeRestrictionList([IdHexRestriction(self.exit_id)])
+    elif self.use_all_exits:
+      self.exit_rstr = NodeRestrictionList(
+        [FlagsRestriction(["Running","Fast"], ["BadExit"])])
+    else:
+      self.exit_rstr = NodeRestrictionList(
+        [PercentileRestriction(nonentry_skip, nonentry_fast, sorted_r),
+         FlagsRestriction(["Running","Fast"], ["BadExit"])])
+
+    if self.extra_node_rstr:
+      entry_rstr.add_restriction(self.extra_node_rstr)
+      mid_rstr.add_restriction(self.extra_node_rstr)
+      self.exit_rstr.add_restriction(self.extra_node_rstr)
+
+    # GeoIP configuration
+    if self.geoip_config:
+      # Every node needs country_code 
+      entry_rstr.add_restriction(CountryCodeRestriction())
+      mid_rstr.add_restriction(CountryCodeRestriction())
+      self.exit_rstr.add_restriction(CountryCodeRestriction())
+      
+      # Specified countries for different positions
+      if self.geoip_config.entry_country:
+        entry_rstr.add_restriction(CountryRestriction(self.geoip_config.entry_country))
+      if self.geoip_config.middle_country:
+        mid_rstr.add_restriction(CountryRestriction(self.geoip_config.middle_country))
+      if self.geoip_config.exit_country:
+        self.exit_rstr.add_restriction(CountryRestriction(self.geoip_config.exit_country))
+
+      # Excluded countries
+      if self.geoip_config.excludes:
+        plog("INFO", "Excluded countries: " + str(self.geoip_config.excludes))
+        if len(self.geoip_config.excludes) > 0:
+          entry_rstr.add_restriction(ExcludeCountriesRestriction(self.geoip_config.excludes))
+          mid_rstr.add_restriction(ExcludeCountriesRestriction(self.geoip_config.excludes))
+          self.exit_rstr.add_restriction(ExcludeCountriesRestriction(self.geoip_config.excludes))
+      
+      # Unique countries set? None --> pass
+      if self.geoip_config.unique_countries != None:
+        if self.geoip_config.unique_countries:
+          # If True: unique countries 
+          self.path_rstr.add_restriction(UniqueCountryRestriction())
+        else:
+          # False: use the same country for all nodes in a path
+          self.path_rstr.add_restriction(SingleCountryRestriction())
+      
+      # Specify max number of continent crossings, None means UniqueContinents
+      if self.geoip_config.continent_crossings == None:
+        self.path_rstr.add_restriction(UniqueContinentRestriction())
+      else: self.path_rstr.add_restriction(ContinentRestriction(self.geoip_config.continent_crossings))
+      # Should even work in combination with continent crossings
+      if self.geoip_config.ocean_crossings != None:
+        self.path_rstr.add_restriction(OceanPhobicRestriction(self.geoip_config.ocean_crossings))
+
+    # This is kind of hokey..
+    if self.order_exits:
+      if self.__ordered_exit_gen:
+        exitgen = self.__ordered_exit_gen
+        exitgen.reset_restriction(self.exit_rstr)
+      else:
+        exitgen = self.__ordered_exit_gen = \
+          OrderedExitGenerator(80, sorted_r, self.exit_rstr)
+    elif self.uniform:
+      exitgen = ExactUniformGenerator(sorted_r, self.exit_rstr)
+    else:
+      exitgen = BwWeightedGenerator(sorted_r, self.exit_rstr, self.pathlen, exit=True)
+
+    if self.uniform:
+      self.path_selector = PathSelector(
+         ExactUniformGenerator(sorted_r, entry_rstr),
+         ExactUniformGenerator(sorted_r, mid_rstr),
+         exitgen, self.path_rstr)
+    else:
+      # Remove ConserveExitsRestriction for entry and middle positions
+      # by removing the OrNodeRestriction that contains it...
+      # FIXME: This is a landmine for a poor soul to hit.
+      # Then again, most of the rest of this function is, too.
+      entry_rstr.del_restriction(OrNodeRestriction)
+      mid_rstr.del_restriction(OrNodeRestriction)
+      self.path_selector = PathSelector(
+         BwWeightedGenerator(sorted_r, entry_rstr, self.pathlen,
+                             guard=self.use_guards),
+         BwWeightedGenerator(sorted_r, mid_rstr, self.pathlen),
+         exitgen, self.path_rstr)
+      return
+
+  def _set_exit(self, exit_name):
+    # sets an exit, if bad, sets bad_exit
+    exit_id = None
+    if exit_name:
+      if exit_name[0] == '$':
+        exit_id = exit_name
+      elif exit_name in self.consensus.name_to_key:
+        exit_id = self.consensus.name_to_key[exit_name]
+    self.exit_id = exit_id
+
+  def set_exit(self, exit_name):
+    self._set_exit(exit_name)
+    self.exit_rstr.clear()
+    if not self.exit_id:
+      plog("NOTICE", "Requested null exit "+str(self.exit_id))
+      self.bad_restrictions = True
+    elif self.exit_id[1:] not in self.consensus.routers:
+      plog("NOTICE", "Requested absent exit "+str(self.exit_id))
+      self.bad_restrictions = True
+    elif self.consensus.routers[self.exit_id[1:]].down:
+      e = self.consensus.routers[self.exit_id[1:]]
+      plog("NOTICE", "Requested downed exit "+str(self.exit_id)+" (bw: "+str(e.bw)+", flags: "+str(e.flags)+")")
+      self.bad_restrictions = True
+    elif self.consensus.routers[self.exit_id[1:]].deleted:
+      e = self.consensus.routers[self.exit_id[1:]]
+      plog("NOTICE", "Requested deleted exit "+str(self.exit_id)+" (bw: "+str(e.bw)+", flags: "+str(e.flags)+", Down: "+str(e.down)+", ref: "+str(e.refcount)+")")
+      self.bad_restrictions = True
+    else:
+      self.exit_rstr.add_restriction(IdHexRestriction(self.exit_id))
+      plog("DEBUG", "Added exit restriction for "+self.exit_id)
+      try:
+        self.path_selector.exit_gen.rebuild()
+        self.bad_restrictions = False
+      except RestrictionError, e:
+        plog("WARN", "Restriction error "+str(e)+" after set_exit")
+        self.bad_restrictions = True
+    return self.bad_restrictions
+
+  def new_consensus(self, consensus):
+    self.consensus = consensus
+    try:
+      self.path_selector.rebuild_gens(self.consensus.sorted_r)
+      if self.exit_id:
+        self.set_exit(self.exit_id)
+    except NoNodesRemain:
+      plog("NOTICE", "No viable nodes in consensus for restrictions.")
+      # Punting + performing reconfigure..")
+      #self.reconfigure(consensus)
+
+  def set_target(self, ip, port):
+    # sets an exit policy, if bad, rasies exception..
+    "Called to update the ExitPolicyRestrictions with a new ip and port"
+    if self.bad_restrictions:
+      plog("WARN", "Requested target with bad restrictions")
+      raise RestrictionError()
+    self.exit_rstr.del_restriction(ExitPolicyRestriction)
+    self.exit_rstr.add_restriction(ExitPolicyRestriction(ip, port))
+    if self.__ordered_exit_gen: self.__ordered_exit_gen.set_port(port)
+    # Try to choose an exit node in the destination country
+    # needs an IP != 255.255.255.255
+    if self.geoip_config and self.geoip_config.echelon:
+      import GeoIPSupport
+      c = GeoIPSupport.get_country(ip)
+      if c:
+        plog("INFO", "[Echelon] IP "+ip+" is in ["+c+"]")
+        self.exit_rstr.del_restriction(CountryRestriction)
+        self.exit_rstr.add_restriction(CountryRestriction(c))
+      else: 
+        plog("INFO", "[Echelon] Could not determine destination country of IP "+ip)
+        # Try to use a backup country
+        if self.geoip_config.exit_country:
+          self.exit_rstr.del_restriction(CountryRestriction) 
+          self.exit_rstr.add_restriction(CountryRestriction(self.geoip_config.exit_country))
+    # Need to rebuild exit generator
+    self.path_selector.exit_gen.rebuild()
+
+  def select_path(self):
+    if self.bad_restrictions:
+      plog("WARN", "Requested target with bad restrictions")
+      raise RestrictionError()
+    return self.path_selector.select_path(self.pathlen)
+
+class Circuit:
+  "Class to describe a circuit"
+  def __init__(self):
+    self.circ_id = 0
+    self.path = [] # routers
+    self.exit = None
+    self.built = False
+    self.failed = False
+    self.dirty = False
+    self.requested_closed = False
+    self.detached_cnt = 0
+    self.last_extended_at = time.time()
+    self.extend_times = []      # List of all extend-durations
+    self.setup_duration = None  # Sum of extend-times
+    self.pending_streams = []   # Which stream IDs are pending us
+    # XXX: Unused.. Need to use for refcounting because
+    # sometimes circuit closed events come before the stream
+    # close and we need to track those failures..
+    self.carried_streams = []
+
+  def id_path(self):
+    "Returns a list of idhex keys for the path of Routers"
+    return map(lambda r: r.idhex, self.path)
+
+class Stream:
+  "Class to describe a stream"
+  def __init__(self, sid, host, port, kind):
+    self.strm_id = sid
+    self.detached_from = [] # circ id #'s
+    self.pending_circ = None
+    self.circ = None
+    self.host = host
+    self.port = port
+    self.kind = kind
+    self.attached_at = 0
+    self.bytes_read = 0
+    self.bytes_written = 0
+    self.failed = False
+    self.ignored = False # Set if PURPOSE=DIR_*
+    self.failed_reason = None # Cheating a little.. Only used by StatsHandler
+
+  def lifespan(self, now):
+    "Returns the age of the stream"
+    return now-self.attached_at
+
+_origsocket = socket.socket
+class _SocketWrapper(socket.socket):
+  """ Ghetto wrapper to workaround python same_slots_added() and 
+      socket __base__ braindamage """
+  pass
+
+class SmartSocket(_SocketWrapper):
+  """ A SmartSocket is a socket that tracks global socket creation
+      for local ports. It has a member StreamSelector that can
+      be used as a PathBuilder stream StreamSelector (see below).
+
+      Most users will want to reset the base class of SocksiPy to
+      use this class:
+      __oldsocket = socket.socket
+      socket.socket = PathSupport.SmartSocket
+      import SocksiPy
+      socket.socket = __oldsocket
+   """
+  port_table = set()
+  _table_lock = threading.Lock()
+
+  def connect(self, args):
+    ret = super(SmartSocket, self).connect(args)
+    myaddr = self.getsockname()
+    self.__local_addr = myaddr[0]+":"+str(myaddr[1])
+    SmartSocket._table_lock.acquire()
+    assert(self.__local_addr not in SmartSocket.port_table)
+    SmartSocket.port_table.add(myaddr[0]+":"+str(myaddr[1]))
+    SmartSocket._table_lock.release()
+    plog("DEBUG", "Added "+self.__local_addr+" to our local port list")
+    return ret
+
+  def connect_ex(self, args):
+    ret = super(SmartSocket, self).connect_ex(args)
+    myaddr = ret.getsockname()
+    self.__local_addr = myaddr[0]+":"+str(myaddr[1])
+    SmartSocket._table_lock.acquire()
+    assert(self.__local_addr not in SmartSocket.port_table)
+    SmartSocket.port_table.add(myaddr[0]+":"+str(myaddr[1]))
+    SmartSocket._table_lock.release()
+    plog("DEBUG", "Added "+self.__local_addr+" to our local port list")
+    return ret
+
+  def __del__(self):
+    SmartSocket._table_lock.acquire()
+    SmartSocket.port_table.remove(self.__local_addr)
+    SmartSocket._table_lock.release()
+    plog("DEBUG", "Removed "+self.__local_addr+" from our local port list")
+
+  def table_size():
+    SmartSocket._table_lock.acquire()
+    ret = len(SmartSocket.port_table)
+    SmartSocket._table_lock.release()
+    return ret
+  table_size = Callable(table_size)
+
+  def clear_port_table():
+    """ WARNING: Calling this periodically is a *really good idea*.
+        Relying on __del__ can expose you to race conditions on garbage
+        collection between your processes. """
+    SmartSocket._table_lock.acquire()
+    for i in list(SmartSocket.port_table):
+      plog("DEBUG", "Cleared "+i+" from our local port list")
+      SmartSocket.port_table.remove(i)
+    SmartSocket._table_lock.release()
+  clear_port_table = Callable(clear_port_table)
+
+  def StreamSelector(host, port):
+    to_test = host+":"+str(port)
+    SmartSocket._table_lock.acquire()
+    ret = (to_test in SmartSocket.port_table)
+    SmartSocket._table_lock.release()
+    return ret
+  StreamSelector = Callable(StreamSelector)
+
+
+def StreamSelector(host, port):
+  """ A StreamSelector is a function that takes a host and a port as
+      arguments (parsed from Tor's SOURCE_ADDR field in STREAM NEW
+      events) and decides if it is a stream from this process or not.
+
+      This StreamSelector is just a placeholder that always returns True.
+      When you define your own, be aware that you MUST DO YOUR OWN
+      LOCKING inside this function, as it is called from the Eventhandler
+      thread.
+
+      See PathSupport.SmartSocket.StreamSelctor for an actual
+      implementation.
+
+  """
+  return True
+
+# TODO: Make passive "PathWatcher" so people can get aggregate 
+# node reliability stats for normal usage without us attaching streams
+# Can use __metaclass__ and type
+
+class PathBuilder(TorCtl.ConsensusTracker):
+  """
+  PathBuilder implementation. Handles circuit construction, subject
+  to the constraints of the SelectionManager selmgr.
+  
+  Do not access this object from other threads. Instead, use the 
+  schedule_* functions to schedule work to be done in the thread
+  of the EventHandler.
+  """
+  def __init__(self, c, selmgr, RouterClass=TorCtl.Router,
+               strm_selector=StreamSelector):
+    """Constructor. 'c' is a Connection, 'selmgr' is a SelectionManager,
+    and 'RouterClass' is a class that inherits from Router and is used
+    to create annotated Routers."""
+    TorCtl.ConsensusTracker.__init__(self, c, RouterClass)
+    self.last_exit = None
+    self.new_nym = False
+    self.resolve_port = 0
+    self.num_circuits = 1
+    self.circuits = {}
+    self.streams = {}
+    self.selmgr = selmgr
+    self.selmgr.reconfigure(self.current_consensus())
+    self.imm_jobs = Queue.Queue()
+    self.low_prio_jobs = Queue.Queue()
+    self.run_all_jobs = False
+    self.do_reconfigure = False
+    self.strm_selector = strm_selector
+    plog("INFO", "Read "+str(len(self.sorted_r))+"/"+str(len(self.ns_map))+" routers")
+
+  def schedule_immediate(self, job):
+    """
+    Schedules an immediate job to be run before the next event is
+    processed.
+    """
+    assert(self.c.is_live())
+    self.imm_jobs.put(job)
+
+  def schedule_low_prio(self, job):
+    """
+    Schedules a job to be run when a non-time critical event arrives.
+    """
+    assert(self.c.is_live())
+    self.low_prio_jobs.put(job)
+
+  def reset(self):
+    """
+    Resets accumulated state. Currently only clears the 
+    ExactUniformGenerator state.
+    """
+    plog("DEBUG", "Resetting _generated values for ExactUniformGenerator")
+    for r in self.routers.itervalues():
+      for g in xrange(0, len(r._generated)):
+        r._generated[g] = 0
+
+  def is_urgent_event(event):
+    # If event is stream:NEW*/DETACHED or circ BUILT/FAILED, 
+    # it is high priority and requires immediate action.
+    if isinstance(event, TorCtl.CircuitEvent):
+      if event.status in ("BUILT", "FAILED", "CLOSED"):
+        return True
+    elif isinstance(event, TorCtl.StreamEvent):
+      if event.status in ("NEW", "NEWRESOLVE", "DETACHED"):
+        return True
+    return False
+  is_urgent_event = Callable(is_urgent_event)
+
+  def schedule_selmgr(self, job):
+    """
+    Schedules an immediate job to be run before the next event is
+    processed. Also notifies the selection manager that it needs
+    to update itself.
+    """
+    assert(self.c.is_live())
+    def notlambda(this):
+      job(this.selmgr)
+      this.do_reconfigure = True
+    self.schedule_immediate(notlambda)
+
+     
+  def heartbeat_event(self, event):
+    """This function handles dispatching scheduled jobs. If you 
+       extend PathBuilder and want to implement this function for 
+       some reason, be sure to call the parent class"""
+    while not self.imm_jobs.empty():
+      imm_job = self.imm_jobs.get_nowait()
+      imm_job(self)
+    
+    if self.do_reconfigure:
+      self.selmgr.reconfigure(self.current_consensus())
+      self.do_reconfigure = False
+    
+    if self.run_all_jobs:
+      while not self.low_prio_jobs.empty() and self.run_all_jobs:
+        imm_job = self.low_prio_jobs.get_nowait()
+        imm_job(self)
+      self.run_all_jobs = False
+      return
+
+    # If event is stream:NEW*/DETACHED or circ BUILT/FAILED, 
+    # don't run low prio jobs.. No need to delay streams for them.
+    if PathBuilder.is_urgent_event(event): return
+   
+    # Do the low prio jobs one at a time in case a 
+    # higher priority event is queued   
+    if not self.low_prio_jobs.empty():
+      delay_job = self.low_prio_jobs.get_nowait()
+      delay_job(self)
+
+  def build_path(self):
+    """ Get a path from the SelectionManager's PathSelector, can be used 
+        e.g. for generating paths without actually creating any circuits """
+    return self.selmgr.select_path()
+
+  def close_all_streams(self, reason):
+    """ Close all open streams """
+    for strm in self.streams.itervalues():
+      if not strm.ignored:
+        try:
+          self.c.close_stream(strm.strm_id, reason)
+        except TorCtl.ErrorReply, e:
+          # This can happen. Streams can timeout before this call.
+          plog("NOTICE", "Error closing stream "+str(strm.strm_id)+": "+str(e))
+
+  def close_all_circuits(self):
+    """ Close all open circuits """
+    for circ in self.circuits.itervalues():
+      self.close_circuit(circ.circ_id)
+
+  def close_circuit(self, id):
+    """ Close a circuit with given id """
+    # TODO: Pass streams to another circ before closing?
+    plog("DEBUG", "Requesting close of circuit id: "+str(id))
+    if self.circuits[id].requested_closed: return
+    self.circuits[id].requested_closed = True
+    try: self.c.close_circuit(id)
+    except TorCtl.ErrorReply, e: 
+      plog("ERROR", "Failed closing circuit " + str(id) + ": " + str(e))
+
+  def circuit_list(self):
+    """ Return an iterator or a list of circuits prioritized for 
+        stream selection."""
+    return self.circuits.itervalues()
+
+  def attach_stream_any(self, stream, badcircs):
+    "Attach a stream to a valid circuit, avoiding any in 'badcircs'"
+    # Newnym, and warn if not built plus pending
+    unattached_streams = [stream]
+    if self.new_nym:
+      self.new_nym = False
+      plog("DEBUG", "Obeying new nym")
+      for key in self.circuits.keys():
+        if (not self.circuits[key].dirty
+            and len(self.circuits[key].pending_streams)):
+          plog("WARN", "New nym called, destroying circuit "+str(key)
+             +" with "+str(len(self.circuits[key].pending_streams))
+             +" pending streams")
+          unattached_streams.extend(self.circuits[key].pending_streams)
+          self.circuits[key].pending_streams = []
+        # FIXME: Consider actually closing circ if no streams.
+        self.circuits[key].dirty = True
+      
+    for circ in self.circuit_list():
+      if circ.built and not circ.requested_closed and not circ.dirty \
+          and circ.circ_id not in badcircs:
+        # XXX: Fails for 'tor-resolve 530.19.6.80' -> NEWRESOLVE
+        if circ.exit.will_exit_to(stream.host, stream.port):
+          try:
+            self.c.attach_stream(stream.strm_id, circ.circ_id)
+            stream.pending_circ = circ # Only one possible here
+            circ.pending_streams.append(stream)
+          except TorCtl.ErrorReply, e:
+            # No need to retry here. We should get the failed
+            # event for either the circ or stream next
+            plog("WARN", "Error attaching new stream: "+str(e.args))
+            return
+          break
+    else:
+      circ = None
+      try:
+        self.selmgr.set_target(stream.host, stream.port)
+        circ = self.c.build_circuit(self.selmgr.select_path())
+      except RestrictionError, e:
+        # XXX: Dress this up a bit
+        self.last_exit = None
+        # Kill this stream
+        plog("WARN", "Closing impossible stream "+str(stream.strm_id)+" ("+str(e)+")")
+        try:
+          self.c.close_stream(stream.strm_id, "4") # END_STREAM_REASON_EXITPOLICY
+        except TorCtl.ErrorReply, e:
+          plog("WARN", "Error closing stream: "+str(e))
+        return
+      except TorCtl.ErrorReply, e:
+        plog("WARN", "Error building circ: "+str(e.args))
+        self.last_exit = None
+        # Kill this stream
+        plog("NOTICE", "Closing stream "+str(stream.strm_id))
+        try:
+          self.c.close_stream(stream.strm_id, "5") # END_STREAM_REASON_DESTROY
+        except TorCtl.ErrorReply, e:
+          plog("WARN", "Error closing stream: "+str(e))
+        return
+      for u in unattached_streams:
+        plog("DEBUG",
+           "Attaching "+str(u.strm_id)+" pending build of "+str(circ.circ_id))
+        u.pending_circ = circ
+      circ.pending_streams.extend(unattached_streams)
+      self.circuits[circ.circ_id] = circ
+    self.last_exit = circ.exit
+    plog("DEBUG", "Set last exit to "+self.last_exit.idhex)
+
+  def circ_status_event(self, c):
+    output = [str(time.time()-c.arrived_at), c.event_name, str(c.circ_id),
+              c.status]
+    if c.path: output.append(",".join(c.path))
+    if c.reason: output.append("REASON=" + c.reason)
+    if c.remote_reason: output.append("REMOTE_REASON=" + c.remote_reason)
+    plog("DEBUG", " ".join(output))
+    # Circuits we don't control get built by Tor
+    if c.circ_id not in self.circuits:
+      plog("DEBUG", "Ignoring circ " + str(c.circ_id))
+      return
+    if c.status == "EXTENDED":
+      self.circuits[c.circ_id].last_extended_at = c.arrived_at
+    elif c.status == "FAILED" or c.status == "CLOSED":
+      # XXX: Can still get a STREAM FAILED for this circ after this
+      circ = self.circuits[c.circ_id]
+      for r in circ.path:
+        r.refcount -= 1
+        plog("DEBUG", "Close refcount "+str(r.refcount)+" for "+r.idhex)
+        if r.deleted and r.refcount == 0:
+          # XXX: This shouldn't happen with StatsRouters.. 
+          if r.__class__.__name__ == "StatsRouter":
+            plog("WARN", "Purging expired StatsRouter "+r.idhex)
+          else:
+            plog("INFO", "Purging expired router "+r.idhex)
+          del self.routers[r.idhex]
+          self.selmgr.new_consensus(self.current_consensus())
+      del self.circuits[c.circ_id]
+      for stream in circ.pending_streams:
+        # If it was built, let Tor decide to detach or fail the stream
+        if not circ.built:
+          plog("DEBUG", "Finding new circ for " + str(stream.strm_id))
+          self.attach_stream_any(stream, stream.detached_from)
+        else:
+          plog("NOTICE", "Waiting on Tor to hint about stream "+str(stream.strm_id)+" on closed circ "+str(circ.circ_id))
+    elif c.status == "BUILT":
+      self.circuits[c.circ_id].built = True
+      try:
+        for stream in self.circuits[c.circ_id].pending_streams:
+          self.c.attach_stream(stream.strm_id, c.circ_id)
+      except TorCtl.ErrorReply, e:
+        # No need to retry here. We should get the failed
+        # event for either the circ or stream in the next event
+        plog("NOTICE", "Error attaching pending stream: "+str(e.args))
+        return
+
+  def stream_status_event(self, s):
+    output = [str(time.time()-s.arrived_at), s.event_name, str(s.strm_id),
+              s.status, str(s.circ_id),
+          s.target_host, str(s.target_port)]
+    if s.reason: output.append("REASON=" + s.reason)
+    if s.remote_reason: output.append("REMOTE_REASON=" + s.remote_reason)
+    if s.purpose: output.append("PURPOSE=" + s.purpose)
+    if s.source_addr: output.append("SOURCE_ADDR="+s.source_addr)
+    if not re.match(r"\d+.\d+.\d+.\d+", s.target_host):
+      s.target_host = "255.255.255.255" # ignore DNS for exit policy check
+
+    # Hack to ignore Tor-handled streams
+    if s.strm_id in self.streams and self.streams[s.strm_id].ignored:
+      if s.status == "CLOSED":
+        plog("DEBUG", "Deleting ignored stream: " + str(s.strm_id))
+        del self.streams[s.strm_id]
+      else:
+        plog("DEBUG", "Ignoring stream: " + str(s.strm_id))
+      return
+
+    plog("DEBUG", " ".join(output))
+    # XXX: Copy s.circ_id==0 check+reset from StatsSupport here too?
+
+    if s.status == "NEW" or s.status == "NEWRESOLVE":
+      if s.status == "NEWRESOLVE" and not s.target_port:
+        s.target_port = self.resolve_port
+      if s.circ_id == 0:
+        self.streams[s.strm_id] = Stream(s.strm_id, s.target_host, s.target_port, s.status)
+      elif s.strm_id not in self.streams:
+        plog("NOTICE", "Got new stream "+str(s.strm_id)+" with circuit "
+                       +str(s.circ_id)+" already attached.")
+        self.streams[s.strm_id] = Stream(s.strm_id, s.target_host, s.target_port, s.status)
+        self.streams[s.strm_id].circ_id = s.circ_id
+
+      # Remember Tor-handled streams (Currently only directory streams)
+
+      if s.purpose and s.purpose.find("DIR_") == 0:
+        self.streams[s.strm_id].ignored = True
+        plog("DEBUG", "Ignoring stream: " + str(s.strm_id))
+        return
+      elif s.source_addr:
+        src_addr = s.source_addr.split(":")
+        src_addr[1] = int(src_addr[1])
+        if not self.strm_selector(*src_addr):
+          self.streams[s.strm_id].ignored = True
+          plog("INFO", "Ignoring foreign stream: " + str(s.strm_id))
+          return
+      if s.circ_id == 0:
+        self.attach_stream_any(self.streams[s.strm_id],
+                   self.streams[s.strm_id].detached_from)
+    elif s.status == "DETACHED":
+      if s.strm_id not in self.streams:
+        plog("WARN", "Detached stream "+str(s.strm_id)+" not found")
+        self.streams[s.strm_id] = Stream(s.strm_id, s.target_host,
+                      s.target_port, "NEW")
+      # FIXME Stats (differentiate Resolved streams also..)
+      if not s.circ_id:
+        if s.reason == "TIMEOUT" or s.reason == "EXITPOLICY":
+          plog("NOTICE", "Stream "+str(s.strm_id)+" detached with "+s.reason)
+        else:
+          plog("WARN", "Stream "+str(s.strm_id)+" detached from no circuit with reason: "+str(s.reason))
+      else:
+        self.streams[s.strm_id].detached_from.append(s.circ_id)
+
+      if self.streams[s.strm_id].pending_circ and \
+           self.streams[s.strm_id] in \
+                  self.streams[s.strm_id].pending_circ.pending_streams:
+        self.streams[s.strm_id].pending_circ.pending_streams.remove(
+                                                self.streams[s.strm_id])
+      self.streams[s.strm_id].pending_circ = None
+      self.attach_stream_any(self.streams[s.strm_id],
+                   self.streams[s.strm_id].detached_from)
+    elif s.status == "SUCCEEDED":
+      if s.strm_id not in self.streams:
+        plog("NOTICE", "Succeeded stream "+str(s.strm_id)+" not found")
+        return
+      if s.circ_id and self.streams[s.strm_id].pending_circ.circ_id != s.circ_id:
+        # Hrmm.. this can happen on a new-nym.. Very rare, putting warn
+        # in because I'm still not sure this is correct
+        plog("WARN", "Mismatch of pending: "
+          +str(self.streams[s.strm_id].pending_circ.circ_id)+" vs "
+          +str(s.circ_id))
+        # This can happen if the circuit existed before we started up
+        if s.circ_id in self.circuits:
+          self.streams[s.strm_id].circ = self.circuits[s.circ_id]
+        else:
+          plog("NOTICE", "Stream "+str(s.strm_id)+" has unknown circuit: "+str(s.circ_id))
+      else:
+        self.streams[s.strm_id].circ = self.streams[s.strm_id].pending_circ
+      self.streams[s.strm_id].pending_circ.pending_streams.remove(self.streams[s.strm_id])
+      self.streams[s.strm_id].pending_circ = None
+      self.streams[s.strm_id].attached_at = s.arrived_at
+    elif s.status == "FAILED" or s.status == "CLOSED":
+      # FIXME stats
+      if s.strm_id not in self.streams:
+        plog("NOTICE", "Failed stream "+str(s.strm_id)+" not found")
+        return
+
+      # XXX: Can happen on timeout
+      if not s.circ_id:
+        if s.reason == "TIMEOUT" or s.reason == "EXITPOLICY":
+          plog("NOTICE", "Stream "+str(s.strm_id)+" "+s.status+" with "+s.reason)
+        else:
+          plog("WARN", "Stream "+str(s.strm_id)+" "+s.status+" from no circuit with reason: "+str(s.reason))
+
+      # We get failed and closed for each stream. OK to return 
+      # and let the closed do the cleanup
+      if s.status == "FAILED":
+        # Avoid busted circuits that will not resolve or carry
+        # traffic. 
+        self.streams[s.strm_id].failed = True
+        if s.circ_id in self.circuits: self.circuits[s.circ_id].dirty = True
+        elif s.circ_id != 0: 
+          plog("WARN", "Failed stream "+str(s.strm_id)+" on unknown circ "+str(s.circ_id))
+        return
+
+      if self.streams[s.strm_id].pending_circ:
+        self.streams[s.strm_id].pending_circ.pending_streams.remove(self.streams[s.strm_id])
+      del self.streams[s.strm_id]
+    elif s.status == "REMAP":
+      if s.strm_id not in self.streams:
+        plog("WARN", "Remap id "+str(s.strm_id)+" not found")
+      else:
+        if not re.match(r"\d+.\d+.\d+.\d+", s.target_host):
+          s.target_host = "255.255.255.255"
+          plog("NOTICE", "Non-IP remap for "+str(s.strm_id)+" to "
+                   + s.target_host)
+        self.streams[s.strm_id].host = s.target_host
+        self.streams[s.strm_id].port = s.target_port
+
+  def stream_bw_event(self, s):
+    output = [str(time.time()-s.arrived_at), s.event_name, str(s.strm_id),
+              str(s.bytes_written),
+              str(s.bytes_read)]
+    if not s.strm_id in self.streams:
+      plog("DEBUG", " ".join(output))
+      plog("WARN", "BW event for unknown stream id: "+str(s.strm_id))
+    else:
+      if not self.streams[s.strm_id].ignored:
+        plog("DEBUG", " ".join(output))
+      self.streams[s.strm_id].bytes_read += s.bytes_read
+      self.streams[s.strm_id].bytes_written += s.bytes_written
+
+  def new_consensus_event(self, n):
+    TorCtl.ConsensusTracker.new_consensus_event(self, n)
+    self.selmgr.new_consensus(self.current_consensus())
+
+  def new_desc_event(self, d):
+    if TorCtl.ConsensusTracker.new_desc_event(self, d):
+      self.selmgr.new_consensus(self.current_consensus())
+
+  def bandwidth_event(self, b): pass # For heartbeat only..
+
+################### CircuitHandler #############################
+
+class CircuitHandler(PathBuilder):
+  """ CircuitHandler that extends from PathBuilder to handle multiple
+      circuits as opposed to just one. """
+  def __init__(self, c, selmgr, num_circuits, RouterClass):
+    """Constructor. 'c' is a Connection, 'selmgr' is a SelectionManager,
+    'num_circuits' is the number of circuits to keep in the pool,
+    and 'RouterClass' is a class that inherits from Router and is used
+    to create annotated Routers."""
+    PathBuilder.__init__(self, c, selmgr, RouterClass)
+    # Set handler to the connection here to 
+    # not miss any circuit events on startup
+    c.set_event_handler(self)
+    self.num_circuits = num_circuits    # Size of the circuit pool
+    self.check_circuit_pool()           # Bring up the pool of circs
+    
+  def check_circuit_pool(self):
+    """ Init or check the status of the circuit-pool """
+    # Get current number of circuits
+    n = len(self.circuits.values())
+    i = self.num_circuits-n
+    if i > 0:
+      plog("INFO", "Checked pool of circuits: we need to build " + 
+         str(i) + " circuits")
+    # Schedule (num_circs-n) circuit-buildups
+    while (n < self.num_circuits):      
+      # TODO: Should mimic Tor's learning here
+      self.build_circuit("255.255.255.255", 80) 
+      plog("DEBUG", "Scheduled circuit No. " + str(n+1))
+      n += 1
+
+  def build_circuit(self, host, port):
+    """ Build a circuit """
+    circ = None
+    while circ == None:
+      try:
+        self.selmgr.set_target(host, port)
+        circ = self.c.build_circuit(self.selmgr.select_path())
+        self.circuits[circ.circ_id] = circ
+        return circ
+      except RestrictionError, e:
+        # XXX: Dress this up a bit
+        traceback.print_exc()
+        plog("ERROR", "Impossible restrictions: "+str(e))
+      except TorCtl.ErrorReply, e:
+        traceback.print_exc()
+        plog("WARN", "Error building circuit: " + str(e.args))
+
+  def circ_status_event(self, c):
+    """ Handle circuit status events """
+    output = [c.event_name, str(c.circ_id), c.status]
+    if c.path: output.append(",".join(c.path))
+    if c.reason: output.append("REASON=" + c.reason)
+    if c.remote_reason: output.append("REMOTE_REASON=" + c.remote_reason)
+    plog("DEBUG", " ".join(output))
+    
+    # Circuits we don't control get built by Tor
+    if c.circ_id not in self.circuits:
+      plog("DEBUG", "Ignoring circuit " + str(c.circ_id) + 
+         " (controlled by Tor)")
+      return
+    
+    # EXTENDED
+    if c.status == "EXTENDED":
+      # Compute elapsed time
+      extend_time = c.arrived_at-self.circuits[c.circ_id].last_extended_at
+      self.circuits[c.circ_id].extend_times.append(extend_time)
+      plog("INFO", "Circuit " + str(c.circ_id) + " extended in " + 
+         str(extend_time) + " sec")
+      self.circuits[c.circ_id].last_extended_at = c.arrived_at
+    
+    # FAILED & CLOSED
+    elif c.status == "FAILED" or c.status == "CLOSED":
+      PathBuilder.circ_status_event(self, c)
+      # Check if there are enough circs
+      self.check_circuit_pool()
+      return
+    # BUILT
+    elif c.status == "BUILT":
+      PathBuilder.circ_status_event(self, c)
+      # Compute duration by summing up extend_times
+      circ = self.circuits[c.circ_id]
+      duration = reduce(lambda x, y: x+y, circ.extend_times, 0.0)
+      plog("INFO", "Circuit " + str(c.circ_id) + " needed " + 
+         str(duration) + " seconds to be built")
+      # Save the duration to the circuit for later use
+      circ.setup_duration = duration
+      
+    # OTHER?
+    else:
+      # If this was e.g. a LAUNCHED
+      pass
+
+################### StreamHandler ##############################
+
+class StreamHandler(CircuitHandler):
+  """ StreamHandler that extends from the CircuitHandler 
+      to handle attaching streams to an appropriate circuit 
+      in the pool. """
+  def __init__(self, c, selmgr, num_circs, RouterClass):
+    CircuitHandler.__init__(self, c, selmgr, num_circs, RouterClass)
+
+  def clear_dns_cache(self):
+    """ Send signal CLEARDNSCACHE """
+    lines = self.c.sendAndRecv("SIGNAL CLEARDNSCACHE\r\n")
+    for _, msg, more in lines:
+      plog("DEBUG", "CLEARDNSCACHE: " + msg)
+
+  def close_stream(self, id, reason):
+    """ Close a stream with given id and reason """
+    self.c.close_stream(id, reason)
+
+  def address_mapped_event(self, event):
+    """ It is necessary to listen to ADDRMAP events to be able to 
+        perform DNS lookups using Tor """
+    output = [event.event_name, event.from_addr, event.to_addr, 
+       time.asctime(event.when)]
+    plog("DEBUG", " ".join(output))
+
+  def unknown_event(self, event):
+    plog("DEBUG", "UNKNOWN EVENT '" + event.event_name + "':" + 
+       event.event_string)
+
+########################## Unit tests ##########################
+
+def do_gen_unit(gen, r_list, weight_bw, num_print):
+  trials = 0
+  for r in r_list:
+    if gen.rstr_list.r_is_ok(r):
+      trials += weight_bw(gen, r)
+  trials = int(trials/1024)
+  
+  print "Running "+str(trials)+" trials"
+
+  # 0. Reset r.chosen = 0 for all routers
+  for r in r_list:
+    r.chosen = 0
+
+  # 1. Generate 'trials' choices:
+  #    1a. r.chosen++
+
+  loglevel = TorUtil.loglevel
+  TorUtil.loglevel = "INFO"
+
+  gen.rewind()
+  rtrs = gen.generate()
+  for i in xrange(1, trials):
+    r = rtrs.next()
+    r.chosen += 1
+
+  TorUtil.loglevel = loglevel
+
+  # 2. Print top num_print routers choices+bandwidth stats+flags
+  i = 0
+  copy_rlist = copy.copy(r_list)
+  copy_rlist.sort(lambda x, y: cmp(y.chosen, x.chosen))
+  for r in copy_rlist:
+    if r.chosen and not gen.rstr_list.r_is_ok(r):
+      print "WARN: Restriction fail at "+r.idhex
+    if not r.chosen and gen.rstr_list.r_is_ok(r):
+      print "WARN: Generation fail at "+r.idhex
+    if not gen.rstr_list.r_is_ok(r): continue
+    flag = ""
+    bw = int(weight_bw(gen, r))
+    if "Exit" in r.flags:
+      flag += "E"
+    if "Guard" in r.flags:
+      flag += "G"
+    print str(r.list_rank)+". "+r.nickname+" "+str(r.bw/1024.0)+"/"+str(bw/1024.0)+": "+str(r.chosen)+", "+flag
+    i += 1
+    if i > num_print: break
+
+def do_unit(rst, r_list, plamb):
+  print "\n"
+  print "-----------------------------------"
+  print rst.r_is_ok.im_class
+  above_i = 0
+  above_bw = 0
+  below_i = 0
+  below_bw = 0
+  for r in r_list:
+    if rst.r_is_ok(r):
+      print r.nickname+" "+plamb(r)+"="+str(rst.r_is_ok(r))+" "+str(r.bw)
+      if r.bw > 400000:
+        above_i = above_i + 1
+        above_bw += r.bw
+      else:
+        below_i = below_i + 1
+        below_bw += r.bw
+        
+  print "Routers above: " + str(above_i) + " bw: " + str(above_bw)
+  print "Routers below: " + str(below_i) + " bw: " + str(below_bw)
+
+# TODO: Tests:
+#  - Test each NodeRestriction and print in/out lines for it
+#  - Test NodeGenerator and reapply NodeRestrictions
+#  - Same for PathSelector and PathRestrictions
+#  - Also Reapply each restriction by hand to path. Verify returns true
+
+if __name__ == '__main__':
+  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+  s.connect((TorUtil.control_host,TorUtil.control_port))
+  c = Connection(s)
+  c.debug(file("control.log", "w"))
+  c.authenticate(TorUtil.control_pass)
+  nslist = c.get_network_status()
+  sorted_rlist = c.read_routers(c.get_network_status())
+
+  sorted_rlist.sort(lambda x, y: cmp(y.bw, x.bw))
+  for i in xrange(len(sorted_rlist)): sorted_rlist[i].list_rank = i
+
+  def flag_weighting(bwgen, r):
+    bw = r.bw
+    if "Exit" in r.flags:
+      bw *= bwgen.exit_weight
+    if "Guard" in r.flags:
+      bw *= bwgen.guard_weight
+    return bw
+
+  def uniform_weighting(bwgen, r):
+    return 10240000
+
+  # XXX: Test OrderedexitGenerators
+  do_gen_unit(
+   UniformGenerator(sorted_rlist,
+                    NodeRestrictionList([PercentileRestriction(20,30,sorted_rlist),
+FlagsRestriction(["Valid"])])),
+                    sorted_rlist, uniform_weighting, 1500)
+
+  
+  do_gen_unit(BwWeightedGenerator(sorted_rlist, FlagsRestriction(["Exit"]),
+                                  3, exit=True),
+              sorted_rlist, flag_weighting, 500)
+  
+  do_gen_unit(BwWeightedGenerator(sorted_rlist, FlagsRestriction(["Guard"]),
+              3, guard=True),
+              sorted_rlist, flag_weighting, 500)
+  
+  do_gen_unit(
+   BwWeightedGenerator(sorted_rlist, FlagsRestriction(["Valid"]), 3),
+   sorted_rlist, flag_weighting, 500)
+
+ 
+  for r in sorted_rlist:
+    if r.will_exit_to("211.11.21.22", 465):
+      print r.nickname+" "+str(r.bw)
+
+  do_unit(FlagsRestriction(["Guard"], []), sorted_rlist, lambda r: " ".join(r.flags))
+  do_unit(FlagsRestriction(["Fast"], []), sorted_rlist, lambda r: " ".join(r.flags))
+
+  do_unit(ExitPolicyRestriction("2.11.2.2", 80), sorted_rlist,
+          lambda r: "exits to 80")
+  do_unit(PercentileRestriction(0, 100, sorted_rlist), sorted_rlist,
+          lambda r: "")
+  do_unit(PercentileRestriction(10, 20, sorted_rlist), sorted_rlist,
+          lambda r: "")
+  do_unit(OSRestriction([r"[lL]inux", r"BSD", "Darwin"], []), sorted_rlist,
+          lambda r: r.os)
+  do_unit(OSRestriction([], ["Windows", "Solaris"]), sorted_rlist,
+          lambda r: r.os)
+   
+  do_unit(VersionRangeRestriction("0.1.2.0"), sorted_rlist,
+          lambda r: str(r.version))
+  do_unit(VersionRangeRestriction("0.1.2.0", "0.1.2.5"), sorted_rlist,
+          lambda r: str(r.version))
+  do_unit(VersionIncludeRestriction(["0.1.1.26-alpha", "0.1.2.7-ignored"]),
+          sorted_rlist, lambda r: str(r.version))
+  do_unit(VersionExcludeRestriction(["0.1.1.26"]), sorted_rlist,
+          lambda r: str(r.version))
+
+  do_unit(ConserveExitsRestriction(), sorted_rlist, lambda r: " ".join(r.flags))
+  do_unit(FlagsRestriction([], ["Valid"]), sorted_rlist, lambda r: " ".join(r.flags))
+
+  do_unit(IdHexRestriction("$FFCB46DB1339DA84674C70D7CB586434C4370441"),
+          sorted_rlist, lambda r: r.idhex)
+
+  rl =  [AtLeastNNodeRestriction([ExitPolicyRestriction("255.255.255.255", 80), ExitPolicyRestriction("255.255.255.255", 443), ExitPolicyRestriction("255.255.255.255", 6667)], 2), FlagsRestriction([], ["BadExit"])]
+
+  exit_rstr = NodeRestrictionList(rl)
+
+  ug = UniformGenerator(sorted_rlist, exit_rstr)
+
+  ug.rewind()
+  rlist = []
+  for r in ug.generate():
+    print "Checking: " + r.nickname
+    for rs in rl:
+      if not rs.r_is_ok(r):
+        raise PathError()
+    if not "Exit" in r.flags:
+      print "No exit in flags of "+r.idhex
+      for e in r.exitpolicy:
+        print " "+str(e)
+      print " 80: "+str(r.will_exit_to("255.255.255.255", 80))
+      print " 443: "+str(r.will_exit_to("255.255.255.255", 443))
+      print " 6667: "+str(r.will_exit_to("255.255.255.255", 6667))
+
+    ug.mark_chosen(r)
+    rlist.append(r)
+  for r in sorted_rlist:
+    if "Exit" in r.flags and not r in rlist:
+      print r.idhex+" is an exit not in rl!"
+        

Added: arm/dependencies/TorCtl/README
===================================================================
--- arm/dependencies/TorCtl/README	                        (rev 0)
+++ arm/dependencies/TorCtl/README	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,42 @@
+                         TorCtl Python Bindings
+
+
+TorCtl is a python Tor controller with extensions to support path
+building and various constraints on node and path selection, as well as
+statistics gathering.
+
+Apps can hook into the TorCtl package at whatever level they wish.
+
+The lowest level of interaction is to use the TorCtl module
+(TorCtl/TorCtl.py). Typically this is done by importing TorCtl.TorCtl
+and creating a TorCtl.Connection and extending from TorCtl.EventHandler.
+This class receives Tor controller events packaged into python classes
+from a TorCtl.Connection.
+
+The next level up is to use the TorCtl.PathSupport module. This is done
+by importing TorCtl.PathSupport and instantiating or extending from
+PathSupport.PathBuilder, which itself extends from TorCtl.EventHandler.
+This class handles circuit construction and stream attachment subject to
+policies defined by PathSupport.NodeRestrictor and
+PathSupport.PathRestrictor implementations.
+
+If you are interested in gathering statistics, you can instead
+instantiate or extend from StatsSupport.StatsHandler, which is
+again an event handler with hooks to record statistics on circuit
+creation, stream bandwidth, and circuit failure information.
+
+All of these modules are pydoced. For more detailed information than
+the above overview, you can do:
+
+# pydoc TorCtl
+# pydoc PathSupport
+# pydoc StatsSupport
+
+There is a minimalistic example of usage of the basic TorCtl.Connection
+and TorCtl.EventHandler in run_example() in TorCtl.py in this directory.
+Other components also have unit tests at the end of their source files.
+
+For more extensive examples of the PathSupport and StatsSupport
+interfaces, see the TorFlow project at git url:
+
+git clone git://git.torproject.org/git/torflow.git

Added: arm/dependencies/TorCtl/SQLSupport.py
===================================================================
--- arm/dependencies/TorCtl/SQLSupport.py	                        (rev 0)
+++ arm/dependencies/TorCtl/SQLSupport.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,1083 @@
+#!/usr/bin/python
+
+"""
+
+Support classes for statisics gathering in SQL Databases
+
+DOCDOC
+
+"""
+
+import socket
+import sys
+import time
+import datetime
+import math
+
+import PathSupport, TorCtl
+from TorUtil import *
+from PathSupport import *
+from TorUtil import meta_port, meta_host, control_port, control_host, control_pass
+from TorCtl import EVENT_TYPE, EVENT_STATE, TorCtlError
+
+import sqlalchemy
+import sqlalchemy.orm.exc
+from sqlalchemy.orm import scoped_session, sessionmaker, eagerload, lazyload, eagerload_all
+from sqlalchemy import create_engine, and_, or_, not_, func
+from sqlalchemy.sql import func,select
+from sqlalchemy.schema import ThreadLocalMetaData,MetaData
+from elixir import *
+
+# Nodes with a ratio below this value will be removed from consideration
+# for higher-valued nodes
+MIN_RATIO=0.5
+
+NO_FPE=2**-50
+
+#################### Model #######################
+
+# In elixir, the session (DB connection) is a property of the model..
+# There can only be one for all of the listeners below that use it
+# See http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabases
+OP=None
+tc_metadata = MetaData()
+tc_metadata.echo=True
+tc_session = scoped_session(sessionmaker(autoflush=True))
+
+def setup_db(db_uri, echo=False, drop=False):
+  tc_engine = create_engine(db_uri, echo=echo)
+  tc_metadata.bind = tc_engine
+  tc_metadata.echo = echo
+
+  setup_all()
+  if drop: drop_all()
+  create_all()
+
+  if sqlalchemy.__version__ < "0.5.0":
+    # DIAF SQLAlchemy. A token gesture at backwards compatibility
+    # wouldn't kill you, you know.
+    tc_session.add = tc_session.save_or_update
+
+class Router(Entity):
+  using_options(shortnames=True, order_by='-published', session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  idhex = Field(CHAR(40), primary_key=True, index=True)
+  orhash = Field(CHAR(27))
+  published = Field(DateTime)
+  nickname = Field(Text)
+
+  os = Field(Text)
+  rate_limited = Field(Boolean)
+  guard = Field(Boolean)
+  exit = Field(Boolean)
+  stable = Field(Boolean)
+  v2dir = Field(Boolean)
+  v3dir = Field(Boolean)
+  hsdir = Field(Boolean)
+
+  bw = Field(Integer)
+  version = Field(Integer)
+  # FIXME: is mutable=False what we want? Do we care?
+  #router = Field(PickleType(mutable=False)) 
+  circuits = ManyToMany('Circuit')
+  streams = ManyToMany('Stream')
+  detached_streams = ManyToMany('Stream')
+  bw_history = OneToMany('BwHistory')
+  stats = OneToOne('RouterStats', inverse="router")
+
+  def from_router(self, router):
+    self.published = router.published
+    self.bw = router.bw
+    self.idhex = router.idhex
+    self.orhash = router.orhash
+    self.nickname = router.nickname
+    # XXX: Temporary hack. router.os can contain unicode, which makes
+    # us barf. Apparently 'Text' types can't have unicode chars?
+    # self.os = router.os
+    self.rate_limited = router.rate_limited
+    self.guard = "Guard" in router.flags
+    self.exit = "Exit" in router.flags
+    self.stable = "Stable" in router.flags
+    self.v2dir = "V2Dir" in router.flags
+    self.v3dir = "V3Dir" in router.flags
+    self.hsdir = "HSDir" in router.flags
+    self.version = router.version.version
+    #self.router = router
+    return self
+
+class BwHistory(Entity):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  router = ManyToOne('Router')
+  bw = Field(Integer)
+  desc_bw = Field(Integer)
+  rank = Field(Integer)
+  pub_time = Field(DateTime)
+
+class Circuit(Entity):
+  using_options(shortnames=True, order_by='-launch_time', session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  routers = ManyToMany('Router')
+  streams = OneToMany('Stream', inverse='circuit')
+  detached_streams = ManyToMany('Stream', inverse='detached_circuits')
+  extensions = OneToMany('Extension', inverse='circ')
+  circ_id = Field(Integer, index=True)
+  launch_time = Field(Float)
+  last_extend = Field(Float)
+
+class FailedCircuit(Circuit):
+  using_mapper_options(save_on_init=False)
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  #failed_extend = ManyToOne('Extension', inverse='circ')
+  fail_reason = Field(Text)
+  fail_time = Field(Float)
+
+class BuiltCircuit(Circuit):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  built_time = Field(Float)
+  tot_delta = Field(Float)
+
+class DestroyedCircuit(Circuit):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  destroy_reason = Field(Text)
+  destroy_time = Field(Float)
+
+class ClosedCircuit(BuiltCircuit):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  closed_time = Field(Float)
+
+class Extension(Entity):
+  using_mapper_options(save_on_init=False)
+  using_options(shortnames=True, order_by='-time', session=tc_session, metadata=tc_metadata)
+  circ = ManyToOne('Circuit', inverse='extensions')
+  from_node = ManyToOne('Router')
+  to_node = ManyToOne('Router')
+  hop = Field(Integer)
+  time = Field(Float)
+  delta = Field(Float)
+
+class FailedExtension(Extension):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  #failed_circ = ManyToOne('FailedCircuit', inverse='failed_extend')
+  using_mapper_options(save_on_init=False)
+  reason = Field(Text)
+
+class Stream(Entity):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_options(shortnames=True, order_by='-start_time')
+  using_mapper_options(save_on_init=False)
+  tgt_host = Field(Text)
+  tgt_port = Field(Integer)
+  circuit = ManyToOne('Circuit', inverse='streams')
+  detached_circuits = ManyToMany('Circuit', inverse='detatched_streams')
+  ignored = Field(Boolean) # Directory streams
+  strm_id = Field(Integer, index=True)
+  start_time = Field(Float)
+  tot_read_bytes = Field(Integer)
+  tot_write_bytes = Field(Integer)
+  init_status = Field(Text)
+  close_reason = Field(Text) # Shared by Failed and Closed. Unused here.
+
+class FailedStream(Stream):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  fail_reason = Field(Text)
+  fail_time = Field(Float)
+
+class ClosedStream(Stream):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  end_time = Field(Float)
+  read_bandwidth = Field(Float)
+  write_bandwidth = Field(Float)
+
+  def tot_bytes(self):
+    return self.tot_read_bytes
+    #return self.tot_read_bytes+self.tot_write_bytes
+
+  def bandwidth(self):
+    return self.tot_bandwidth()
+
+  def tot_bandwidth(self):
+    #return self.read_bandwidth+self.write_bandwidth 
+    return self.read_bandwidth
+
+class RouterStats(Entity):
+  using_options(shortnames=True, session=tc_session, metadata=tc_metadata)
+  using_mapper_options(save_on_init=False)
+  router = ManyToOne('Router', inverse="stats")
+   
+  # Easily derived from BwHistory
+  min_rank = Field(Integer)
+  avg_rank = Field(Float)
+  max_rank = Field(Integer)
+  avg_bw = Field(Float)
+  avg_desc_bw = Field(Float)
+
+  percentile = Field(Float)
+
+  # These can be derived with a single query over 
+  # FailedExtension and Extension
+  circ_fail_to = Field(Float) 
+  circ_fail_from = Field(Float)
+  circ_try_to = Field(Float)
+  circ_try_from = Field(Float)
+
+  circ_from_rate = Field(Float)
+  circ_to_rate = Field(Float)
+  circ_bi_rate = Field(Float)
+
+  circ_to_ratio = Field(Float)
+  circ_from_ratio = Field(Float)
+  circ_bi_ratio = Field(Float)
+
+  avg_first_ext = Field(Float)
+  ext_ratio = Field(Float)
+ 
+  strm_try = Field(Integer)
+  strm_closed = Field(Integer)
+
+  sbw = Field(Float)
+  sbw_dev = Field(Float)
+  sbw_ratio = Field(Float)
+  filt_sbw = Field(Float)
+  filt_sbw_ratio = Field(Float)
+
+  def _compute_stats_relation(stats_clause):
+    for rs in RouterStats.query.\
+                   filter(stats_clause).\
+                   options(eagerload_all('router.circuits.extensions')).\
+                   all():
+      rs.circ_fail_to = 0
+      rs.circ_try_to = 0
+      rs.circ_fail_from = 0
+      rs.circ_try_from = 0
+      tot_extend_time = 0
+      tot_extends = 0
+      for c in rs.router.circuits: 
+        for e in c.extensions: 
+          if e.to_node == r:
+            rs.circ_try_to += 1
+            if isinstance(e, FailedExtension):
+              rs.circ_fail_to += 1
+            elif e.hop == 0:
+              tot_extend_time += e.delta
+              tot_extends += 1
+          elif e.from_node == r:
+            rs.circ_try_from += 1
+            if isinstance(e, FailedExtension):
+              rs.circ_fail_from += 1
+            
+        if isinstance(c, FailedCircuit):
+          pass # TODO: Also count timeouts against earlier nodes?
+        elif isinstance(c, DestroyedCircuit):
+          pass # TODO: Count these somehow..
+
+      if tot_extends > 0: rs.avg_first_ext = (1.0*tot_extend_time)/tot_extends
+      else: rs.avg_first_ext = 0
+      if rs.circ_try_from > 0:
+        rs.circ_from_rate = (1.0*rs.circ_fail_from/rs.circ_try_from)
+      if rs.circ_try_to > 0:
+        rs.circ_to_rate = (1.0*rs.circ_fail_to/rs.circ_try_to)
+      if rs.circ_try_to+rs.circ_try_from > 0:
+        rs.circ_bi_rate = (1.0*rs.circ_fail_to+rs.circ_fail_from)/(rs.circ_try_to+rs.circ_try_from)
+
+      tc_session.add(rs)
+    tc_session.commit()
+  _compute_stats_relation = Callable(_compute_stats_relation)
+
+  def _compute_stats_query(stats_clause):
+    tc_session.clear()
+    # http://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_update
+    to_s = select([func.count(Extension.id)], 
+        and_(stats_clause, Extension.table.c.to_node_idhex
+             == RouterStats.table.c.router_idhex)).as_scalar()
+    from_s = select([func.count(Extension.id)], 
+        and_(stats_clause, Extension.table.c.from_node_idhex
+             == RouterStats.table.c.router_idhex)).as_scalar()
+    f_to_s = select([func.count(FailedExtension.id)], 
+        and_(stats_clause, FailedExtension.table.c.to_node_idhex
+             == RouterStats.table.c.router_idhex,
+             FailedExtension.table.c.row_type=='failedextension')).as_scalar()
+    f_from_s = select([func.count(FailedExtension.id)], 
+        and_(stats_clause, FailedExtension.table.c.from_node_idhex
+                       == RouterStats.table.c.router_idhex,
+             FailedExtension.table.c.row_type=='failedextension')).as_scalar()
+    avg_ext = select([func.avg(Extension.delta)], 
+        and_(stats_clause,
+             Extension.table.c.to_node_idhex==RouterStats.table.c.router_idhex,
+             Extension.table.c.hop==0, 
+             Extension.table.c.row_type=='extension')).as_scalar()
+
+    RouterStats.table.update(stats_clause, values=
+      {RouterStats.table.c.circ_try_to:to_s,
+       RouterStats.table.c.circ_try_from:from_s,
+       RouterStats.table.c.circ_fail_to:f_to_s,
+       RouterStats.table.c.circ_fail_from:f_from_s,
+       RouterStats.table.c.avg_first_ext:avg_ext}).execute()
+
+    RouterStats.table.update(stats_clause, values=
+      {RouterStats.table.c.circ_from_rate:
+         RouterStats.table.c.circ_fail_from/RouterStats.table.c.circ_try_from,
+       RouterStats.table.c.circ_to_rate:
+          RouterStats.table.c.circ_fail_to/RouterStats.table.c.circ_try_to,
+       RouterStats.table.c.circ_bi_rate:
+         (RouterStats.table.c.circ_fail_to+RouterStats.table.c.circ_fail_from)
+                          /
+      (RouterStats.table.c.circ_try_to+RouterStats.table.c.circ_try_from)}).execute()
+
+
+    # TODO: Give the streams relation table a sane name and reduce this too
+    for rs in RouterStats.query.filter(stats_clause).\
+                        options(eagerload('router'),
+                                eagerload('router.detached_streams'),
+                                eagerload('router.streams')).all():
+      tot_bw = 0.0
+      s_cnt = 0
+      tot_bytes = 0.0
+      tot_duration = 0.0
+      for s in rs.router.streams:
+        if isinstance(s, ClosedStream):
+          tot_bytes += s.tot_bytes()
+          tot_duration += s.end_time - s.start_time
+          tot_bw += s.bandwidth()
+          s_cnt += 1
+      # FIXME: Hrmm.. do we want to do weighted avg or pure avg here?
+      # If files are all the same size, it shouldn't matter..
+      if s_cnt > 0:
+        rs.sbw = tot_bw/s_cnt
+      else: rs.sbw = None
+      rs.strm_closed = s_cnt
+      rs.strm_try = len(rs.router.streams)+len(rs.router.detached_streams)
+      if rs.sbw:
+        tot_var = 0.0
+        for s in rs.router.streams:
+          if isinstance(s, ClosedStream):
+            tot_var += (s.bandwidth()-rs.sbw)*(s.bandwidth()-rs.sbw)
+        tot_var /= s_cnt
+        rs.sbw_dev = math.sqrt(tot_var)
+      tc_session.add(rs)
+    tc_session.commit()
+  _compute_stats_query = Callable(_compute_stats_query)
+
+  def _compute_stats(stats_clause):
+    RouterStats._compute_stats_query(stats_clause)
+    #RouterStats._compute_stats_relation(stats_clause)
+  _compute_stats = Callable(_compute_stats)
+
+  def _compute_ranks():
+    tc_session.clear()
+    min_r = select([func.min(BwHistory.rank)],
+        BwHistory.table.c.router_idhex
+            == RouterStats.table.c.router_idhex).as_scalar()
+    avg_r = select([func.avg(BwHistory.rank)],
+        BwHistory.table.c.router_idhex
+            == RouterStats.table.c.router_idhex).as_scalar()
+    max_r = select([func.max(BwHistory.rank)],
+        BwHistory.table.c.router_idhex
+            == RouterStats.table.c.router_idhex).as_scalar()
+    avg_bw = select([func.avg(BwHistory.bw)],
+        BwHistory.table.c.router_idhex
+            == RouterStats.table.c.router_idhex).as_scalar()
+    avg_desc_bw = select([func.avg(BwHistory.desc_bw)],
+        BwHistory.table.c.router_idhex
+            == RouterStats.table.c.router_idhex).as_scalar()
+
+    RouterStats.table.update(values=
+       {RouterStats.table.c.min_rank:min_r,
+        RouterStats.table.c.avg_rank:avg_r,
+        RouterStats.table.c.max_rank:max_r,
+        RouterStats.table.c.avg_bw:avg_bw,
+        RouterStats.table.c.avg_desc_bw:avg_desc_bw}).execute()
+
+    #min_avg_rank = select([func.min(RouterStats.avg_rank)]).as_scalar()
+    max_avg_rank = select([func.max(RouterStats.avg_rank)]).as_scalar()
+
+    RouterStats.table.update(values=
+       {RouterStats.table.c.percentile:
+            (100.0*RouterStats.table.c.avg_rank)/max_avg_rank}).execute()
+    tc_session.commit()
+  _compute_ranks = Callable(_compute_ranks)
+
+  def _compute_ratios(stats_clause):
+    tc_session.clear()
+    avg_from_rate = select([func.avg(RouterStats.circ_from_rate)],
+                           stats_clause).as_scalar()
+    avg_to_rate = select([func.avg(RouterStats.circ_to_rate)],
+                           stats_clause).as_scalar()
+    avg_bi_rate = select([func.avg(RouterStats.circ_bi_rate)],
+                           stats_clause).as_scalar()
+    avg_ext = select([func.avg(RouterStats.avg_first_ext)],
+                           stats_clause).as_scalar()
+    avg_sbw = select([func.avg(RouterStats.sbw)],
+                           stats_clause).as_scalar()
+
+    RouterStats.table.update(stats_clause, values=
+       {RouterStats.table.c.circ_from_ratio:
+         (1-RouterStats.table.c.circ_from_rate)/(1-avg_from_rate),
+        RouterStats.table.c.circ_to_ratio:
+         (1-RouterStats.table.c.circ_to_rate)/(1-avg_to_rate),
+        RouterStats.table.c.circ_bi_ratio:
+         (1-RouterStats.table.c.circ_bi_rate)/(1-avg_bi_rate),
+        RouterStats.table.c.ext_ratio:
+         avg_ext/RouterStats.table.c.avg_first_ext,
+        RouterStats.table.c.sbw_ratio:
+         RouterStats.table.c.sbw/avg_sbw}).execute()
+    tc_session.commit()
+  _compute_ratios = Callable(_compute_ratios)
+
+  def _compute_filtered_relational(min_ratio, stats_clause, filter_clause):
+    badrouters = RouterStats.query.filter(stats_clause).filter(filter_clause).\
+                   filter(RouterStats.sbw_ratio < min_ratio).all()
+
+    # TODO: Turn this into a single query....
+    for rs in RouterStats.query.filter(stats_clause).\
+          options(eagerload_all('router.streams.circuit.routers')).all():
+      tot_sbw = 0
+      sbw_cnt = 0
+      for s in rs.router.streams:
+        if isinstance(s, ClosedStream):
+          skip = False
+          #for br in badrouters:
+          #  if br != rs:
+          #    if br.router in s.circuit.routers:
+          #      skip = True
+          if not skip:
+            # Throw out outliers < mean 
+            # (too much variance for stddev to filter much)
+            if rs.strm_closed == 1 or s.bandwidth() >= rs.sbw:
+              tot_sbw += s.bandwidth()
+              sbw_cnt += 1
+
+      if sbw_cnt: rs.filt_sbw = tot_sbw/sbw_cnt
+      else: rs.filt_sbw = None
+      tc_session.add(rs)
+    if sqlalchemy.__version__ < "0.5.0":
+      avg_sbw = RouterStats.query.filter(stats_clause).avg(RouterStats.filt_sbw)
+    else:
+      avg_sbw = tc_session.query(func.avg(RouterStats.filt_sbw)).filter(stats_clause).scalar()
+    for rs in RouterStats.query.filter(stats_clause).all():
+      if type(rs.filt_sbw) == float and avg_sbw:
+        rs.filt_sbw_ratio = rs.filt_sbw/avg_sbw
+      else:
+        rs.filt_sbw_ratio = None
+      tc_session.add(rs)
+    tc_session.commit()
+  _compute_filtered_relational = Callable(_compute_filtered_relational)
+
+  def _compute_filtered_ratios(min_ratio, stats_clause, filter_clause):
+    RouterStats._compute_filtered_relational(min_ratio, stats_clause, 
+                                             filter_clause)
+    #RouterStats._compute_filtered_query(filter,min_ratio)
+  _compute_filtered_ratios = Callable(_compute_filtered_ratios)
+
+  def reset():
+    tc_session.clear()
+    RouterStats.table.drop()
+    RouterStats.table.create()
+    for r in Router.query.all():
+      rs = RouterStats()
+      rs.router = r
+      r.stats = rs
+      tc_session.add(r)
+    tc_session.commit()
+  reset = Callable(reset)
+
+  def compute(pct_low=0, pct_high=100, stat_clause=None, filter_clause=None):
+    pct_clause = and_(RouterStats.percentile >= pct_low, 
+                         RouterStats.percentile < pct_high)
+    if stat_clause:
+      stat_clause = and_(pct_clause, stat_clause)
+    else:
+      stat_clause = pct_clause
+     
+    RouterStats.reset()
+    RouterStats._compute_ranks() # No filters. Ranks are independent
+    RouterStats._compute_stats(stat_clause)
+    RouterStats._compute_ratios(stat_clause)
+    RouterStats._compute_filtered_ratios(MIN_RATIO, stat_clause, filter_clause)
+    tc_session.commit()
+  compute = Callable(compute)  
+
+  def write_stats(f, pct_low=0, pct_high=100, order_by=None, recompute=False, stat_clause=None, filter_clause=None, disp_clause=None):
+
+    if not order_by:
+      order_by=RouterStats.avg_first_ext
+
+    if recompute:
+      RouterStats.compute(pct_low, pct_high, stat_clause, filter_clause)
+
+    pct_clause = and_(RouterStats.percentile >= pct_low, 
+                         RouterStats.percentile < pct_high)
+
+    # This is Fail City and sqlalchemy is running for mayor.
+    if sqlalchemy.__version__ < "0.5.0":
+      circ_from_rate = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.circ_from_rate)
+      circ_to_rate = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.circ_to_rate)
+      circ_bi_rate = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.circ_bi_rate)
+
+      avg_first_ext = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.avg_first_ext)
+      sbw = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.sbw)
+      filt_sbw = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.filt_sbw)
+      percentile = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.percentile)
+    else:
+      circ_from_rate = tc_session.query(func.avg(RouterStats.circ_from_rate)).filter(pct_clause).filter(stat_clause).scalar()
+      circ_to_rate = tc_session.query(func.avg(RouterStats.circ_to_rate)).filter(pct_clause).filter(stat_clause).scalar()
+      circ_bi_rate = tc_session.query(func.avg(RouterStats.circ_bi_rate)).filter(pct_clause).filter(stat_clause).scalar()
+      
+      avg_first_ext = tc_session.query(func.avg(RouterStats.avg_first_ext)).filter(pct_clause).filter(stat_clause).scalar()
+      sbw = tc_session.query(func.avg(RouterStats.sbw)).filter(pct_clause).filter(stat_clause).scalar()
+      filt_sbw = tc_session.query(func.avg(RouterStats.filt_sbw)).filter(pct_clause).filter(stat_clause).scalar()
+      percentile = tc_session.query(func.avg(RouterStats.percentile)).filter(pct_clause).filter(stat_clause).scalar()
+
+    def cvt(a,b,c=1):
+      if type(a) == float: return round(a/c,b)
+      elif type(a) == int: return a
+      elif type(a) == type(None): return "None"
+      else: return type(a)
+
+    sql_key = """SQLSupport Statistics:
+    CF=Circ From Rate          CT=Circ To Rate      CB=Circ To/From Rate
+    CE=Avg 1st Ext time (s)    SB=Avg Stream BW     FB=Filtered stream bw
+    SD=Strm BW stddev          CC=Circ To Attempts  ST=Strem attempts
+    SC=Streams Closed OK       RF=Circ From Ratio   RT=Circ To Ratio     
+    RB=Circ To/From Ratio      RE=1st Ext Ratio     RS=Stream BW Ratio   
+    RF=Filt Stream Ratio       PR=Percentile Rank\n\n"""
+ 
+    f.write(sql_key)
+    f.write("Average Statistics:\n")
+    f.write("   CF="+str(cvt(circ_from_rate,2)))
+    f.write("  CT="+str(cvt(circ_to_rate,2)))
+    f.write("  CB="+str(cvt(circ_bi_rate,2)))
+    f.write("  CE="+str(cvt(avg_first_ext,2)))
+    f.write("  SB="+str(cvt(sbw,2,1024)))
+    f.write("  FB="+str(cvt(filt_sbw,2,1024)))
+    f.write("  PR="+str(cvt(percentile,2))+"\n\n\n")
+
+    for s in RouterStats.query.filter(pct_clause).filter(stat_clause).\
+             filter(disp_clause).order_by(order_by).all():
+      f.write(s.router.idhex+" ("+s.router.nickname+")\n")
+      f.write("   CF="+str(cvt(s.circ_from_rate,2)))
+      f.write("  CT="+str(cvt(s.circ_to_rate,2)))
+      f.write("  CB="+str(cvt(s.circ_bi_rate,2)))
+      f.write("  CE="+str(cvt(s.avg_first_ext,2)))
+      f.write("  SB="+str(cvt(s.sbw,2,1024)))
+      f.write("  FB="+str(cvt(s.filt_sbw,2,1024)))
+      f.write("  SD="+str(cvt(s.sbw_dev,2,1024))+"\n")
+      f.write("   RF="+str(cvt(s.circ_from_ratio,2)))
+      f.write("  RT="+str(cvt(s.circ_to_ratio,2)))
+      f.write("  RB="+str(cvt(s.circ_bi_ratio,2)))
+      f.write("  RE="+str(cvt(s.ext_ratio,2)))
+      f.write("  RS="+str(cvt(s.sbw_ratio,2)))
+      f.write("  RF="+str(cvt(s.filt_sbw_ratio,2)))
+      f.write("  PR="+str(cvt(s.percentile,1))+"\n")
+      f.write("   CC="+str(cvt(s.circ_try_to,1)))
+      f.write("  ST="+str(cvt(s.strm_try, 1)))
+      f.write("  SC="+str(cvt(s.strm_closed, 1))+"\n\n")
+
+    f.flush()
+  write_stats = Callable(write_stats)  
+  
+
+  def write_bws(f, pct_low=0, pct_high=100, order_by=None, recompute=False, stat_clause=None, filter_clause=None, disp_clause=None):
+    if not order_by:
+      order_by=RouterStats.avg_first_ext
+
+    if recompute:
+      RouterStats.compute(pct_low, pct_high, stat_clause, filter_clause)
+
+    pct_clause = and_(RouterStats.percentile >= pct_low, 
+                         RouterStats.percentile < pct_high)
+
+    # This is Fail City and sqlalchemy is running for mayor.
+    if sqlalchemy.__version__ < "0.5.0":
+      sbw = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.sbw)
+      filt_sbw = RouterStats.query.filter(pct_clause).filter(stat_clause).avg(RouterStats.filt_sbw)
+    else:
+      sbw = tc_session.query(func.avg(RouterStats.sbw)).filter(pct_clause).filter(stat_clause).scalar()
+      filt_sbw = tc_session.query(func.avg(RouterStats.filt_sbw)).filter(pct_clause).filter(stat_clause).scalar()
+
+    f.write(str(int(time.time()))+"\n")
+
+    def cvt(a,b,c=1):
+      if type(a) == float: return int(round(a/c,b))
+      elif type(a) == int: return a
+      elif type(a) == type(None): return "None"
+      else: return type(a)
+
+    for s in RouterStats.query.filter(pct_clause).filter(stat_clause).\
+           filter(disp_clause).order_by(order_by).all():
+      f.write("node_id=$"+s.router.idhex+" nick="+s.router.nickname)
+      f.write(" strm_bw="+str(cvt(s.sbw,0)))
+      f.write(" filt_bw="+str(cvt(s.filt_sbw,0)))
+      f.write(" desc_bw="+str(int(cvt(s.avg_desc_bw,0))))
+      f.write(" ns_bw="+str(int(cvt(s.avg_bw,0)))+"\n")
+
+    f.flush()
+  write_bws = Callable(write_bws)  
+    
+
+##################### End Model ####################
+
+#################### Model Support ################
+def reset_all():
+  # Need to keep routers around.. 
+  for r in Router.query.all():
+    # This appears to be needed. the relation tables do not get dropped 
+    # automatically.
+    r.circuits = []
+    r.streams = []
+    r.detached_streams = []
+    r.bw_history = [] 
+    r.stats = None
+    tc_session.add(r)
+
+  tc_session.commit()
+  tc_session.clear()
+
+  BwHistory.table.drop() # Will drop subclasses
+  Extension.table.drop()
+  Stream.table.drop() 
+  Circuit.table.drop()
+  RouterStats.table.drop()
+
+  RouterStats.table.create()
+  BwHistory.table.create() 
+  Extension.table.create()
+  Stream.table.create() 
+  Circuit.table.create()
+
+  tc_session.commit()
+
+  #for r in Router.query.all():
+  #  if len(r.bw_history) or len(r.circuits) or len(r.streams) or r.stats:
+  #    plog("WARN", "Router still has dropped data!")
+
+  plog("NOTICE", "Reset all SQL stats")
+
+##################### End Model Support ####################
+
+class ConsensusTrackerListener(TorCtl.DualEventListener):
+  def __init__(self):
+    TorCtl.DualEventListener.__init__(self)
+    self.last_desc_at = time.time()+60 # Give tor some time to start up
+    self.consensus = None
+    self.wait_for_signal = False
+
+  CONSENSUS_DONE = 0x7fffffff
+
+  # TODO: What about non-running routers and uptime information?
+  def _update_rank_history(self, idlist):
+    plog("INFO", "Consensus change... Updating rank history")
+    for idhex in idlist:
+      if idhex not in self.consensus.routers: continue
+      rc = self.consensus.routers[idhex]
+      if rc.down: continue
+      try:
+        r = Router.query.options(eagerload('bw_history')).filter_by(
+                                    idhex=idhex).with_labels().one()
+        bwh = BwHistory(router=r, rank=rc.list_rank, bw=rc.bw,
+                        desc_bw=rc.desc_bw, pub_time=r.published)
+        r.bw_history.append(bwh)
+        #tc_session.add(bwh)
+        tc_session.add(r)
+      except sqlalchemy.orm.exc.NoResultFound:
+        plog("WARN", "No descriptor found for consenus router "+str(idhex))
+
+    plog("INFO", "Consensus history updated.")
+    tc_session.commit()
+
+  def _update_db(self, idlist):
+    # FIXME: It is tempting to delay this as well, but we need
+    # this info to be present immediately for circuit construction...
+    plog("INFO", "Consensus change... Updating db")
+    for idhex in idlist:
+      if idhex in self.consensus.routers:
+        rc = self.consensus.routers[idhex]
+        r = Router.query.filter_by(idhex=rc.idhex).first()
+        if r and r.orhash == rc.orhash:
+          # We already have it stored. (Possible spurious NEWDESC)
+          continue
+        if not r: r = Router()
+        r.from_router(rc)
+        tc_session.add(r)
+    plog("INFO", "Consensus db updated")
+    tc_session.commit()
+
+  def update_consensus(self):
+    plog("INFO", "Updating DB with full consensus.")
+    self.consensus = self.parent_handler.current_consensus()
+    self._update_db(self.consensus.ns_map.iterkeys())
+
+  def set_parent(self, parent_handler):
+    if not isinstance(parent_handler, TorCtl.ConsensusTracker):
+      raise TorCtlError("ConsensusTrackerListener can only be attached to ConsensusTracker instances")
+    TorCtl.DualEventListener.set_parent(self, parent_handler)
+
+  def heartbeat_event(self, e):
+    # This sketchiness is to ensure we have an accurate history
+    # of each router's rank+bandwidth for the entire duration of the run..
+    if e.state == EVENT_STATE.PRELISTEN:
+      if not self.consensus: 
+        global OP
+        OP = Router.query.filter_by(
+                 idhex="0000000000000000000000000000000000000000").first()
+        if not OP:
+          OP = Router(idhex="0000000000000000000000000000000000000000", 
+                    orhash="000000000000000000000000000", 
+                    nickname="!!TorClient", 
+                    published=datetime.datetime.utcnow())
+          tc_session.add(OP)
+          tc_session.commit()
+        self.update_consensus()
+      # XXX: This hack exists because update_rank_history is expensive.
+      # However, even if we delay it till the end of the consensus update, 
+      # it still delays event processing for up to 30 seconds on a fast 
+      # machine.
+      # 
+      # The correct way to do this is give SQL processing
+      # to a dedicated worker thread that pulls events off of a secondary
+      # queue, that way we don't block stream handling on this processing.
+      # The problem is we are pretty heavily burdened with the need to 
+      # stay in sync with our parent event handler. A queue will break this 
+      # coupling (even if we could get all the locking right).
+      #
+      # A lighterweight hack might be to just make the scanners pause
+      # on a condition used to signal we are doing this (and other) heavy 
+      # lifting. We could have them possibly check self.last_desc_at..
+      if not self.wait_for_signal and e.arrived_at - self.last_desc_at > 60.0:
+        if self.consensus.consensus_count  < 0.95*(len(self.consensus.ns_map)):
+          plog("INFO", "Not enough router descriptors: "
+                       +str(self.consensus.consensus_count)+"/"
+                       +str(len(self.consensus.ns_map)))
+        elif not PathSupport.PathBuilder.is_urgent_event(e):
+          plog("INFO", "Newdesc timer is up. Assuming we have full consensus")
+          self._update_rank_history(self.consensus.ns_map.iterkeys())
+          self.last_desc_at = ConsensusTrackerListener.CONSENSUS_DONE
+
+  def new_consensus_event(self, n):
+    if n.state == EVENT_STATE.POSTLISTEN:
+      self.last_desc_at = n.arrived_at
+      self.update_consensus()
+
+  def new_desc_event(self, d): 
+    if d.state == EVENT_STATE.POSTLISTEN:
+      self.last_desc_at = d.arrived_at
+      self.consensus = self.parent_handler.current_consensus()
+      self._update_db(d.idlist)
+
+class CircuitListener(TorCtl.PreEventListener):
+  def set_parent(self, parent_handler):
+    if not filter(lambda f: f.__class__ == ConsensusTrackerListener, 
+                  parent_handler.post_listeners):
+       raise TorCtlError("CircuitListener needs a ConsensusTrackerListener")
+    TorCtl.PreEventListener.set_parent(self, parent_handler)
+    # TODO: This is really lame. We only know the extendee of a circuit
+    # if we have built the path ourselves. Otherwise, Tor keeps it a
+    # secret from us. This prevents us from properly tracking failures
+    # for normal Tor usage.
+    if isinstance(parent_handler, PathSupport.PathBuilder):
+      self.track_parent = True
+    else:
+      self.track_parent = False
+
+  def circ_status_event(self, c):
+    if self.track_parent and c.circ_id not in self.parent_handler.circuits:
+      return # Ignore circuits that aren't ours
+    # TODO: Hrmm, consider making this sane in TorCtl.
+    if c.reason: lreason = c.reason
+    else: lreason = "NONE"
+    if c.remote_reason: rreason = c.remote_reason
+    else: rreason = "NONE"
+    reason = c.event_name+":"+c.status+":"+lreason+":"+rreason
+
+    output = [str(c.arrived_at), str(time.time()-c.arrived_at), c.event_name, str(c.circ_id), c.status]
+    if c.path: output.append(",".join(c.path))
+    if c.reason: output.append("REASON=" + c.reason)
+    if c.remote_reason: output.append("REMOTE_REASON=" + c.remote_reason)
+    plog("DEBUG", " ".join(output))
+  
+    if c.status == "LAUNCHED":
+      circ = Circuit(circ_id=c.circ_id,launch_time=c.arrived_at,
+                     last_extend=c.arrived_at)
+      if self.track_parent:
+        for r in self.parent_handler.circuits[c.circ_id].path:
+          rq = Router.query.options(eagerload('circuits')).filter_by(
+                                idhex=r.idhex).with_labels().one()
+          circ.routers.append(rq) 
+          #rq.circuits.append(circ) # done automagically?
+          #tc_session.add(rq)
+      tc_session.add(circ)
+      tc_session.commit()
+    elif c.status == "EXTENDED":
+      circ = Circuit.query.options(eagerload('extensions')).filter_by(
+                       circ_id = c.circ_id).first()
+      if not circ: return # Skip circuits from before we came online
+
+      e = Extension(circ=circ, hop=len(c.path)-1, time=c.arrived_at)
+
+      if len(c.path) == 1:
+        e.from_node = OP
+      else:
+        r_ext = c.path[-2]
+        if r_ext[0] != '$': r_ext = self.parent_handler.name_to_key[r_ext]
+        e.from_node = Router.query.filter_by(idhex=r_ext[1:]).one()
+
+      r_ext = c.path[-1]
+      if r_ext[0] != '$': r_ext = self.parent_handler.name_to_key[r_ext]
+
+      e.to_node = Router.query.filter_by(idhex=r_ext[1:]).one()
+      if not self.track_parent:
+        # FIXME: Eager load here?
+        circ.routers.append(e.to_node)
+        e.to_node.circuits.append(circ)
+        tc_session.add(e.to_node)
+ 
+      e.delta = c.arrived_at - circ.last_extend
+      circ.last_extend = c.arrived_at
+      circ.extensions.append(e)
+      tc_session.add(e)
+      tc_session.add(circ)
+      tc_session.commit()
+    elif c.status == "FAILED":
+      circ = Circuit.query.filter_by(circ_id = c.circ_id).first()
+      if not circ: return # Skip circuits from before we came online
+        
+      circ.expunge()
+      if isinstance(circ, BuiltCircuit):
+        # Convert to destroyed circuit
+        Circuit.table.update(Circuit.id ==
+                  circ.id).execute(row_type='destroyedcircuit')
+        circ = DestroyedCircuit.query.filter_by(id=circ.id).one()
+        circ.destroy_reason = reason
+        circ.destroy_time = c.arrived_at
+      else:
+        # Convert to failed circuit
+        Circuit.table.update(Circuit.id ==
+                  circ.id).execute(row_type='failedcircuit')
+        circ = FailedCircuit.query.options(
+                  eagerload('extensions')).filter_by(id=circ.id).one()
+        circ.fail_reason = reason
+        circ.fail_time = c.arrived_at
+        e = FailedExtension(circ=circ, hop=len(c.path), time=c.arrived_at)
+
+        if len(c.path) == 0:
+          e.from_node = OP
+        else:
+          r_ext = c.path[-1]
+          if r_ext[0] != '$': r_ext = self.parent_handler.name_to_key[r_ext]
+ 
+          e.from_node = Router.query.filter_by(idhex=r_ext[1:]).one()
+
+        if self.track_parent:
+          r=self.parent_handler.circuits[c.circ_id].path[len(c.path)]
+          e.to_node = Router.query.filter_by(idhex=r.idhex).one()
+        else:
+          e.to_node = None # We have no idea..
+
+        e.delta = c.arrived_at - circ.last_extend
+        e.reason = reason
+        circ.extensions.append(e)
+        circ.fail_time = c.arrived_at
+        tc_session.add(e)
+
+      tc_session.add(circ)
+      tc_session.commit()
+    elif c.status == "BUILT":
+      circ = Circuit.query.filter_by(
+                     circ_id = c.circ_id).first()
+      if not circ: return # Skip circuits from before we came online
+
+      circ.expunge()
+      # Convert to built circuit
+      Circuit.table.update(Circuit.id ==
+                circ.id).execute(row_type='builtcircuit')
+      circ = BuiltCircuit.query.filter_by(id=circ.id).one()
+      
+      circ.built_time = c.arrived_at
+      circ.tot_delta = c.arrived_at - circ.launch_time
+      tc_session.add(circ)
+      tc_session.commit()
+    elif c.status == "CLOSED":
+      circ = BuiltCircuit.query.filter_by(circ_id = c.circ_id).first()
+      if circ:
+        circ.expunge()
+        if lreason in ("REQUESTED", "FINISHED", "ORIGIN"):
+          # Convert to closed circuit
+          Circuit.table.update(Circuit.id ==
+                    circ.id).execute(row_type='closedcircuit')
+          circ = ClosedCircuit.query.filter_by(id=circ.id).one()
+          circ.closed_time = c.arrived_at
+        else:
+          # Convert to destroyed circuit
+          Circuit.table.update(Circuit.id ==
+                    circ.id).execute(row_type='destroyedcircuit')
+          circ = DestroyedCircuit.query.filter_by(id=circ.id).one()
+          circ.destroy_reason = reason
+          circ.destroy_time = c.arrived_at
+        tc_session.add(circ)
+        tc_session.commit()
+
+class StreamListener(CircuitListener):
+  def stream_bw_event(self, s):
+    strm = Stream.query.filter_by(strm_id = s.strm_id).first()
+    if strm and strm.start_time and strm.start_time < s.arrived_at:
+      plog("DEBUG", "Got stream bw: "+str(s.strm_id))
+      strm.tot_read_bytes += s.bytes_read
+      strm.tot_write_bytes += s.bytes_written
+      tc_session.add(strm)
+      tc_session.commit()
+ 
+  def stream_status_event(self, s):
+    if s.reason: lreason = s.reason
+    else: lreason = "NONE"
+    if s.remote_reason: rreason = s.remote_reason
+    else: rreason = "NONE"
+
+    if s.status in ("NEW", "NEWRESOLVE"):
+      strm = Stream(strm_id=s.strm_id, tgt_host=s.target_host, 
+                    tgt_port=s.target_port, init_status=s.status,
+                    tot_read_bytes=0, tot_write_bytes=0)
+      tc_session.add(strm)
+      tc_session.commit()
+      return
+
+    strm = Stream.query.filter_by(strm_id = s.strm_id).first()
+    if self.track_parent and \
+      (s.strm_id not in self.parent_handler.streams or \
+           self.parent_handler.streams[s.strm_id].ignored):
+      if strm:
+        tc_session.delete(strm)
+        tc_session.commit()
+      return # Ignore streams that aren't ours
+
+    if not strm: 
+      plog("NOTICE", "Ignoring prior stream "+str(s.strm_id))
+      return # Ignore prior streams
+
+    reason = s.event_name+":"+s.status+":"+lreason+":"+rreason+":"+strm.init_status
+
+    if s.status == "SENTCONNECT":
+      # New circuit
+      strm.circuit = Circuit.query.filter_by(circ_id=s.circ_id).first()
+      if not strm.circuit:
+        plog("NOTICE", "Ignoring prior stream "+str(strm.strm_id)+" with old circuit "+str(s.circ_id))
+        tc_session.delete(strm)
+        tc_session.commit()
+        return
+    else:
+      circ = None
+      if s.circ_id:
+        circ = Circuit.query.filter_by(circ_id=s.circ_id).first()
+      elif self.track_parent:
+        circ = self.parent_handler.streams[s.strm_id].circ
+        if not circ: circ = self.parent_handler.streams[s.strm_id].pending_circ
+        if circ:
+          circ = Circuit.query.filter_by(circ_id=circ.circ_id).first()
+
+      if not circ:
+        plog("WARN", "No circuit for "+str(s.strm_id)+" circ: "+str(s.circ_id))
+
+      if not strm.circuit:
+        plog("INFO", "No stream circuit for "+str(s.strm_id)+" circ: "+str(s.circ_id))
+        strm.circuit = circ
+
+      # XXX: Verify circ id matches stream.circ
+    
+    if s.status == "SUCCEEDED":
+      strm.start_time = s.arrived_at
+      for r in strm.circuit.routers: 
+        plog("DEBUG", "Added router "+r.idhex+" to stream "+str(s.strm_id))
+        r.streams.append(strm)
+        tc_session.add(r)
+      tc_session.add(strm)
+      tc_session.commit()
+    elif s.status == "DETACHED":
+      for r in strm.circuit.routers:
+        r.detached_streams.append(strm)
+        tc_session.add(r)
+      #strm.detached_circuits.append(strm.circuit)
+      strm.circuit.detached_streams.append(strm)
+      strm.circuit.streams.remove(strm)
+      strm.circuit = None
+      tc_session.add(strm)
+      tc_session.commit()
+    elif s.status == "FAILED":
+      strm.expunge()
+      # Convert to destroyed circuit
+      Stream.table.update(Stream.id ==
+                strm.id).execute(row_type='failedstream')
+      strm = FailedStream.query.filter_by(id=strm.id).one()
+      strm.fail_time = s.arrived_at
+      strm.fail_reason = reason
+      tc_session.add(strm)
+      tc_session.commit()
+    elif s.status == "CLOSED":
+      if isinstance(strm, FailedStream):
+        strm.close_reason = reason
+      else:
+        strm.expunge()
+        if not (lreason == "DONE" or (lreason == "END" and rreason == "DONE")):
+          # Convert to destroyed circuit
+          Stream.table.update(Stream.id ==
+                    strm.id).execute(row_type='failedstream')
+          strm = FailedStream.query.filter_by(id=strm.id).one()
+          strm.fail_time = s.arrived_at
+        else: 
+          # Convert to destroyed circuit
+          Stream.table.update(Stream.id ==
+                    strm.id).execute(row_type='closedstream')
+          strm = ClosedStream.query.filter_by(id=strm.id).one()
+          strm.read_bandwidth = strm.tot_read_bytes/(s.arrived_at-strm.start_time)
+          strm.write_bandwidth = strm.tot_write_bytes/(s.arrived_at-strm.start_time)
+          strm.end_time = s.arrived_at
+          plog("DEBUG", "Stream "+str(strm.strm_id)+" xmitted "+str(strm.tot_bytes()))
+        strm.close_reason = reason
+      tc_session.add(strm)
+      tc_session.commit()
+
+def run_example(host, port):
+  """ Example of basic TorCtl usage. See PathSupport for more advanced
+      usage.
+  """
+  print "host is %s:%d"%(host,port)
+  setup_db("sqlite:///torflow.sqlite", echo=False)
+
+  #print tc_session.query(((func.count(Extension.id)))).filter(and_(FailedExtension.table.c.row_type=='extension', FailedExtension.table.c.from_node_idhex == "7CAA2F5F998053EF5D2E622563DEB4A6175E49AC")).one()
+  #return
+  #for e in Extension.query.filter(FailedExtension.table.c.row_type=='extension').all():
+  #  if e.from_node: print "From: "+e.from_node.idhex+" "+e.from_node.nickname
+  #  if e.to_node: print "To: "+e.to_node.idhex+" "+e.to_node.nickname
+  #return
+
+  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+  s.connect((host,port))
+  c = Connection(s)
+  th = c.launch_thread()
+  c.authenticate(control_pass)
+  c.set_event_handler(TorCtl.ConsensusTracker(c))
+  c.add_event_listener(ConsensusTrackerListener())
+  c.add_event_listener(CircuitListener())
+
+  print `c.extend_circuit(0,["moria1"])`
+  try:
+    print `c.extend_circuit(0,[""])`
+  except TorCtl.ErrorReply: # wtf?
+    print "got error. good."
+  except:
+    print "Strange error", sys.exc_info()[0]
+   
+  c.set_events([EVENT_TYPE.STREAM, EVENT_TYPE.CIRC,
+          EVENT_TYPE.NEWCONSENSUS, EVENT_TYPE.NEWDESC,
+          EVENT_TYPE.ORCONN, EVENT_TYPE.BW], True)
+
+  th.join()
+  return
+
+  
+if __name__ == '__main__':
+  run_example(control_host,control_port)
+

Added: arm/dependencies/TorCtl/ScanSupport.py
===================================================================
--- arm/dependencies/TorCtl/ScanSupport.py	                        (rev 0)
+++ arm/dependencies/TorCtl/ScanSupport.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,251 @@
+import PathSupport
+import SQLSupport
+import threading
+import copy
+import time
+import shutil
+
+from TorUtil import plog
+
+# Note: be careful writing functions for this class. Remember that
+# the PathBuilder has its own thread that it recieves events on
+# independent from your thread that calls into here.
+class ScanHandler(PathSupport.PathBuilder):
+  def set_pct_rstr(self, percent_skip, percent_fast):
+    def notlambda(sm):
+      sm.percent_fast=percent_fast
+      sm.percent_skip=percent_skip
+    self.schedule_selmgr(notlambda)
+
+  def reset_stats(self):
+    def notlambda(this):
+      this.reset()
+    self.schedule_low_prio(notlambda)
+
+  def commit(self):
+    plog("INFO", "Scanner committing jobs...")
+    cond = threading.Condition()
+    def notlambda2(this):
+      cond.acquire()
+      this.run_all_jobs = False
+      plog("INFO", "Commit done.")
+      cond.notify()
+      cond.release()
+
+    def notlambda1(this):
+      plog("INFO", "Committing jobs...")
+      this.run_all_jobs = True
+      self.schedule_low_prio(notlambda2)
+
+    cond.acquire()
+    self.schedule_immediate(notlambda1)
+
+    cond.wait()
+    cond.release()
+    plog("INFO", "Scanner commit done.")
+
+  def close_circuits(self):
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      this.close_all_circuits()
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+
+  def close_streams(self, reason):
+    cond = threading.Condition()
+    plog("NOTICE", "Wedged Tor stream. Closing all streams")
+    def notlambda(this):
+      cond.acquire()
+      this.close_all_streams(reason)
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+
+  def new_exit(self):
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      this.new_nym = True
+      if this.selmgr.bad_restrictions:
+        plog("NOTICE", "Clearing bad restrictions with reconfigure..")
+        this.selmgr.reconfigure(this.current_consensus())
+      lines = this.c.sendAndRecv("SIGNAL CLEARDNSCACHE\r\n")
+      for _,msg,more in lines:
+        plog("DEBUG", msg)
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+
+  def idhex_to_r(self, idhex):
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      if idhex in self.routers:
+        cond._result = self.routers[idhex]
+      else:
+        cond._result = None
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+    return cond._result
+
+  def name_to_idhex(self, nick):
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      if nick in self.name_to_key:
+        cond._result = self.name_to_key[nick]
+      else:
+        cond._result = None
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+    return cond._result
+
+  def rank_to_percent(self, rank):
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      cond._pct = (100.0*rank)/len(this.sorted_r) # lol moar haxx
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+    return cond._pct
+
+  def percent_to_rank(self, pct):
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      cond._rank = int(round((pct*len(this.sorted_r))/100.0,0)) # lol moar haxx
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+    return cond._rank
+
+  def get_exit_node(self):
+    ret = copy.copy(self.last_exit) # GIL FTW
+    if ret:
+      plog("DEBUG", "Got last exit of "+ret.idhex)
+    else:
+      plog("DEBUG", "No last exit.")
+    return ret
+
+  def set_exit_node(self, arg):
+    cond = threading.Condition()
+    exit_name = arg
+    plog("DEBUG", "Got Setexit: "+exit_name)
+    def notlambda(sm):
+      plog("DEBUG", "Job for setexit: "+exit_name)
+      cond.acquire()
+      sm.set_exit(exit_name)
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_selmgr(notlambda)
+    cond.wait()
+    cond.release()
+
+class SQLScanHandler(ScanHandler):
+  def attach_sql_listener(self, db_uri):
+    plog("DEBUG", "Got sqlite: "+db_uri)
+    SQLSupport.setup_db(db_uri, echo=False, drop=True)
+    self.sql_consensus_listener = SQLSupport.ConsensusTrackerListener()
+    self.add_event_listener(self.sql_consensus_listener)
+    self.add_event_listener(SQLSupport.StreamListener())
+
+  def write_sql_stats(self, rfilename=None, stats_filter=None):
+    if not rfilename:
+      rfilename="./data/stats/sql-"+time.strftime("20%y-%m-%d-%H:%M:%S")
+    cond = threading.Condition()
+    def notlambda(h):
+      cond.acquire()
+      SQLSupport.RouterStats.write_stats(file(rfilename, "w"),
+                            0, 100, order_by=SQLSupport.RouterStats.sbw,
+                            recompute=True, disp_clause=stats_filter)
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+
+  def write_strm_bws(self, rfilename=None, slice_num=0, stats_filter=None):
+    if not rfilename:
+      rfilename="./data/stats/bws-"+time.strftime("20%y-%m-%d-%H:%M:%S")
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      f=file(rfilename, "w")
+      f.write("slicenum="+str(slice_num)+"\n")
+      SQLSupport.RouterStats.write_bws(f, 0, 100,
+                            order_by=SQLSupport.RouterStats.sbw,
+                            recompute=False, disp_clause=stats_filter)
+      f.close()
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+
+  def save_sql_file(self, sql_file, new_file):
+    cond = threading.Condition()
+    def notlambda(this):
+      cond.acquire()
+      SQLSupport.tc_session.close()
+      try:
+        shutil.copy(sql_file, new_file)
+      except Exception,e:
+        plog("WARN", "Error moving sql file: "+str(e))
+      SQLSupport.reset_all()
+      cond.notify()
+      cond.release()
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+
+  def wait_for_consensus(self):
+    cond = threading.Condition()
+    def notlambda(this):
+      if this.sql_consensus_listener.last_desc_at \
+                 != SQLSupport.ConsensusTrackerListener.CONSENSUS_DONE:
+        this.sql_consensus_listener.wait_for_signal = False
+        plog("INFO", "Waiting on consensus result: "+str(this.run_all_jobs))
+        this.schedule_low_prio(notlambda)
+      else:
+        cond.acquire()
+        this.sql_consensus_listener.wait_for_signal = True
+        cond.notify()
+        cond.release()
+    plog("DEBUG", "Checking for consensus")
+    cond.acquire()
+    self.schedule_low_prio(notlambda)
+    cond.wait()
+    cond.release()
+    plog("INFO", "Consensus OK")
+
+

Added: arm/dependencies/TorCtl/StatsSupport.py
===================================================================
--- arm/dependencies/TorCtl/StatsSupport.py	                        (rev 0)
+++ arm/dependencies/TorCtl/StatsSupport.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,894 @@
+#!/usr/bin/python
+#StatsSupport.py - functions and classes useful for calculating stream/circuit statistics
+
+"""
+
+Support classes for statisics gathering
+
+The StatsSupport package contains several classes that extend
+PathSupport to gather continuous statistics on the Tor network.
+
+The main entrypoint is to extend or create an instance of the
+StatsHandler class. The StatsHandler extends from
+TorCtl.PathSupport.PathBuilder, which is itself a TorCtl.EventHandler.
+The StatsHandler listens to CIRC and STREAM events and gathers all
+manner of statics on their creation and failure before passing the
+events back up to the PathBuilder code, which manages the actual
+construction and the attachment of streams to circuits.
+
+The package also contains a number of support classes that help gather
+additional statistics on the reliability and performance of routers.
+
+For the purpose of accounting failures, the code tracks two main classes
+of failure: 'actual' failure and 'suspected' failure. The general rule
+is that an actual failure is attributed to the node that directly
+handled the circuit or stream. For streams, this is considered to be the
+exit node. For circuits, it is both the extender and the extendee.
+'Suspected' failures, on the other hand, are attributed to every member
+of the circuit up until the extendee for circuits, and all hops for
+streams.
+
+For bandwidth accounting, the average stream bandwidth and the average
+ratio of stream bandwidth to advertised bandwidth are tracked, and when
+the statistics are written, a Z-test is performed to calculate the
+probabilities of these values assuming a normal distribution. Note,
+however, that it has not been verified that this distribution is
+actually normal. It is likely to be something else (pareto, perhaps?).
+
+"""
+
+import sys
+import re
+import random
+import copy
+import time
+import math
+import traceback
+
+import TorUtil, PathSupport, TorCtl
+from TorUtil import *
+from PathSupport import *
+from TorUtil import meta_port, meta_host, control_port, control_host
+
+class ReasonRouterList:
+  "Helper class to track which Routers have failed for a given reason"
+  def __init__(self, reason):
+    self.reason = reason
+    self.rlist = {}
+
+  def sort_list(self): raise NotImplemented()
+
+  def write_list(self, f):
+    "Write the list of failure counts for this reason 'f'"
+    rlist = self.sort_list()
+    for r in rlist:
+      susp = 0
+      tot_failed = r.circ_failed+r.strm_failed
+      tot_susp = tot_failed+r.circ_suspected+r.strm_suspected
+      f.write(r.idhex+" ("+r.nickname+") F=")
+      if self.reason in r.reason_failed:
+        susp = r.reason_failed[self.reason]
+      f.write(str(susp)+"/"+str(tot_failed))
+      f.write(" S=")
+      if self.reason in r.reason_suspected:
+        susp += r.reason_suspected[self.reason]
+      f.write(str(susp)+"/"+str(tot_susp)+"\n")
+    
+  def add_r(self, r):
+    "Add a router to the list for this reason"
+    self.rlist[r] = 1
+
+  def total_suspected(self):
+    "Get a list of total suspected failures for this reason"
+    # suspected is disjoint from failed. The failed table
+    # may not have an entry
+    def notlambda(x, y):
+      if self.reason in y.reason_suspected:
+        if self.reason in y.reason_failed:
+          return (x + y.reason_suspected[self.reason]
+               + y.reason_failed[self.reason])
+        else:
+          return (x + y.reason_suspected[self.reason])
+      else:
+        if self.reason in y.reason_failed:
+          return (x + y.reason_failed[self.reason])
+        else: return x
+    return reduce(notlambda, self.rlist.iterkeys(), 0)
+
+  def total_failed(self):
+    "Get a list of total failures for this reason"
+    def notlambda(x, y):
+      if self.reason in y.reason_failed:
+        return (x + y.reason_failed[self.reason])
+      else: return x
+    return reduce(notlambda, self.rlist.iterkeys(), 0)
+ 
+class SuspectRouterList(ReasonRouterList):
+  """Helper class to track all routers suspected of failing for a given
+     reason. The main difference between this and the normal
+     ReasonRouterList is the sort order and the verification."""
+  def __init__(self, reason): ReasonRouterList.__init__(self,reason)
+  
+  def sort_list(self):
+    rlist = self.rlist.keys()
+    rlist.sort(lambda x, y: cmp(y.reason_suspected[self.reason],
+                  x.reason_suspected[self.reason]))
+    return rlist
+   
+  def _verify_suspected(self):
+    return reduce(lambda x, y: x + y.reason_suspected[self.reason],
+            self.rlist.iterkeys(), 0)
+
+class FailedRouterList(ReasonRouterList):
+  """Helper class to track all routers that failed for a given
+     reason. The main difference between this and the normal
+     ReasonRouterList is the sort order and the verification."""
+  def __init__(self, reason): ReasonRouterList.__init__(self,reason)
+
+  def sort_list(self):
+    rlist = self.rlist.keys()
+    rlist.sort(lambda x, y: cmp(y.reason_failed[self.reason],
+                  x.reason_failed[self.reason]))
+    return rlist
+
+  def _verify_failed(self):
+    return reduce(lambda x, y: x + y.reason_failed[self.reason],
+            self.rlist.iterkeys(), 0)
+class BandwidthStats:
+  "Class that manages observed bandwidth through a Router"
+  def __init__(self):
+    self.byte_list = []
+    self.duration_list = []
+    self.min_bw = 1e10
+    self.max_bw = 0
+    self.mean = 0
+    self.dev = 0
+
+  def _exp(self): # Weighted avg
+    "Expectation - weighted average of the bandwidth through this node"
+    tot_bw = reduce(lambda x, y: x+y, self.byte_list, 0.0)
+    EX = 0.0
+    for i in xrange(len(self.byte_list)):
+      EX += (self.byte_list[i]*self.byte_list[i])/self.duration_list[i]
+    if tot_bw == 0.0: return 0.0
+    EX /= tot_bw
+    return EX
+
+  def _exp2(self): # E[X^2]
+    "Second moment of the bandwidth"
+    tot_bw = reduce(lambda x, y: x+y, self.byte_list, 0.0)
+    EX = 0.0
+    for i in xrange(len(self.byte_list)):
+      EX += (self.byte_list[i]**3)/(self.duration_list[i]**2)
+    if tot_bw == 0.0: return 0.0
+    EX /= tot_bw
+    return EX
+    
+  def _dev(self): # Weighted dev
+    "Standard deviation of bandwidth"
+    EX = self.mean
+    EX2 = self._exp2()
+    arg = EX2 - (EX*EX)
+    if arg < -0.05:
+      plog("WARN", "Diff of "+str(EX2)+" and "+str(EX)+"^2 is "+str(arg))
+    return math.sqrt(abs(arg))
+
+  def add_bw(self, bytes, duration):
+    "Add an observed transfer of 'bytes' for 'duration' seconds"
+    if not bytes: plog("NOTICE", "No bytes for bandwidth")
+    bytes /= 1024.
+    self.byte_list.append(bytes)
+    self.duration_list.append(duration)
+    bw = bytes/duration
+    plog("DEBUG", "Got bandwidth "+str(bw))
+    if self.min_bw > bw: self.min_bw = bw
+    if self.max_bw < bw: self.max_bw = bw
+    self.mean = self._exp()
+    self.dev = self._dev()
+
+
+class StatsRouter(TorCtl.Router):
+  "Extended Router to handle statistics markup"
+  def __init__(self, router): # Promotion constructor :)
+    """'Promotion Constructor' that converts a Router directly into a 
+    StatsRouter without a copy."""
+    # TODO: Use __bases__ to do this instead?
+    self.__dict__ = router.__dict__
+    self.reset()
+    # StatsRouters should not be destroyed when Tor forgets about them
+    # Give them an extra refcount:
+    self.refcount += 1
+    plog("DEBUG", "Stats refcount "+str(self.refcount)+" for "+self.idhex)
+
+  def reset(self):
+    "Reset all stats on this Router"
+    self.circ_uncounted = 0
+    self.circ_failed = 0
+    self.circ_succeeded = 0 # disjoint from failed
+    self.circ_suspected = 0
+    self.circ_chosen = 0 # above 4 should add to this
+    self.strm_failed = 0 # Only exits should have these
+    self.strm_succeeded = 0
+    self.strm_suspected = 0 # disjoint from failed
+    self.strm_uncounted = 0
+    self.strm_chosen = 0 # above 4 should add to this
+    self.reason_suspected = {}
+    self.reason_failed = {}
+    self.first_seen = time.time()
+    if "Running" in self.flags:
+      self.became_active_at = self.first_seen
+      self.hibernated_at = 0
+    else:
+      self.became_active_at = 0
+      self.hibernated_at = self.first_seen
+    self.total_hibernation_time = 0
+    self.total_active_uptime = 0
+    self.total_extend_time = 0
+    self.total_extended = 0
+    self.bwstats = BandwidthStats()
+    self.z_ratio = 0
+    self.prob_zr = 0
+    self.z_bw = 0
+    self.prob_zb = 0
+    self.rank_history = []
+    self.bw_history = []
+
+  def was_used(self):
+    """Return True if this router was used in this round"""
+    return self.circ_chosen != 0
+
+  def avg_extend_time(self):
+    """Return the average amount of time it took for this router
+     to extend a circuit one hop"""
+    if self.total_extended:
+      return self.total_extend_time/self.total_extended
+    else: return 0
+
+  def bw_ratio(self):
+    """Return the ratio of the Router's advertised bandwidth to its 
+     observed average stream bandwidth"""
+    bw = self.bwstats.mean
+    if bw == 0.0: return 0
+    else: return self.bw/(1024.*bw)
+
+  def adv_ratio(self): # XXX
+    """Return the ratio of the Router's advertised bandwidth to 
+       the overall average observed bandwith"""
+    bw = StatsRouter.global_bw_mean
+    if bw == 0.0: return 0
+    else: return self.bw/bw
+
+  def avg_rank(self):
+    if not self.rank_history: return self.list_rank
+    return (1.0*sum(self.rank_history))/len(self.rank_history)
+
+  def bw_ratio_ratio(self):
+    bwr = self.bw_ratio()
+    if bwr == 0.0: return 0
+    # (avg_reported_bw/our_reported_bw) *
+    # (our_stream_capacity/avg_stream_capacity)
+    return StatsRouter.global_ratio_mean/bwr 
+
+  def strm_bw_ratio(self):
+    """Return the ratio of the Router's stream capacity to the average
+       stream capacity passed in as 'mean'"""
+    bw = self.bwstats.mean
+    if StatsRouter.global_strm_mean == 0.0: return 0
+    else: return (1.0*bw)/StatsRouter.global_strm_mean
+
+  def circ_fail_rate(self):
+    if self.circ_chosen == 0: return 0
+    return (1.0*self.circ_failed)/self.circ_chosen
+
+  def strm_fail_rate(self):
+    if self.strm_chosen == 0: return 0
+    return (1.0*self.strm_failed)/self.strm_chosen
+
+  def circ_suspect_rate(self):
+    if self.circ_chosen == 0: return 1
+    return (1.0*(self.circ_suspected+self.circ_failed))/self.circ_chosen
+
+  def strm_suspect_rate(self):
+    if self.strm_chosen == 0: return 1
+    return (1.0*(self.strm_suspected+self.strm_failed))/self.strm_chosen
+
+  def circ_suspect_ratio(self):
+    if 1.0-StatsRouter.global_cs_mean <= 0.0: return 0
+    return (1.0-self.circ_suspect_rate())/(1.0-StatsRouter.global_cs_mean)
+
+  def strm_suspect_ratio(self):
+    if 1.0-StatsRouter.global_ss_mean <= 0.0: return 0
+    return (1.0-self.strm_suspect_rate())/(1.0-StatsRouter.global_ss_mean)
+
+  def circ_fail_ratio(self):
+    if 1.0-StatsRouter.global_cf_mean <= 0.0: return 0
+    return (1.0-self.circ_fail_rate())/(1.0-StatsRouter.global_cf_mean)
+
+  def strm_fail_ratio(self):
+    if 1.0-StatsRouter.global_sf_mean <= 0.0: return 0
+    return (1.0-self.strm_fail_rate())/(1.0-StatsRouter.global_sf_mean)
+
+  def current_uptime(self):
+    if self.became_active_at:
+      ret = (self.total_active_uptime+(time.time()-self.became_active_at))
+    else:
+      ret = self.total_active_uptime
+    if ret == 0: return 0.000005 # eh..
+    else: return ret
+        
+  def failed_per_hour(self):
+    """Return the number of circuit extend failures per hour for this 
+     Router"""
+    return (3600.*(self.circ_failed+self.strm_failed))/self.current_uptime()
+
+  # XXX: Seperate suspected from failed in totals 
+  def suspected_per_hour(self):
+    """Return the number of circuits that failed with this router as an
+     earlier hop"""
+    return (3600.*(self.circ_suspected+self.strm_suspected
+          +self.circ_failed+self.strm_failed))/self.current_uptime()
+
+  # These four are for sanity checking
+  def _suspected_per_hour(self):
+    return (3600.*(self.circ_suspected+self.strm_suspected))/self.current_uptime()
+
+  def _uncounted_per_hour(self):
+    return (3600.*(self.circ_uncounted+self.strm_uncounted))/self.current_uptime()
+
+  def _chosen_per_hour(self):
+    return (3600.*(self.circ_chosen+self.strm_chosen))/self.current_uptime()
+
+  def _succeeded_per_hour(self):
+    return (3600.*(self.circ_succeeded+self.strm_succeeded))/self.current_uptime()
+  
+  key = """Metatroller Router Statistics:
+  CC=Circuits Chosen   CF=Circuits Failed      CS=Circuit Suspected
+  SC=Streams Chosen    SF=Streams Failed       SS=Streams Suspected
+  FH=Failed per Hour   SH=Suspected per Hour   ET=avg circuit Extend Time (s)
+  EB=mean BW (K)       BD=BW std Dev (K)       BR=Ratio of observed to avg BW
+  ZB=BW z-test value   PB=Probability(z-bw)    ZR=Ratio z-test value
+  PR=Prob(z-ratio)     SR=Global mean/mean BW  U=Uptime (h)\n"""
+
+  global_strm_mean = 0.0
+  global_strm_dev = 0.0
+  global_ratio_mean = 0.0
+  global_ratio_dev = 0.0
+  global_bw_mean = 0.0
+  global_cf_mean = 0.0
+  global_sf_mean = 0.0
+  global_cs_mean = 0.0
+  global_ss_mean = 0.0
+
+  def __str__(self):
+    return (self.idhex+" ("+self.nickname+")\n"
+    +"   CC="+str(self.circ_chosen)
+      +" CF="+str(self.circ_failed)
+      +" CS="+str(self.circ_suspected+self.circ_failed)
+      +" SC="+str(self.strm_chosen)
+      +" SF="+str(self.strm_failed)
+      +" SS="+str(self.strm_suspected+self.strm_failed)
+      +" FH="+str(round(self.failed_per_hour(),1))
+      +" SH="+str(round(self.suspected_per_hour(),1))+"\n"
+    +"   ET="+str(round(self.avg_extend_time(),1))
+      +" EB="+str(round(self.bwstats.mean,1))
+      +" BD="+str(round(self.bwstats.dev,1))
+      +" ZB="+str(round(self.z_bw,1))
+      +" PB="+(str(round(self.prob_zb,3))[1:])
+      +" BR="+str(round(self.bw_ratio(),1))
+      +" ZR="+str(round(self.z_ratio,1))
+      +" PR="+(str(round(self.prob_zr,3))[1:])
+      +" SR="+(str(round(self.strm_bw_ratio(),1)))
+      +" U="+str(round(self.current_uptime()/3600, 1))+"\n")
+
+  def sanity_check(self):
+    "Makes sure all stats are self-consistent"
+    if (self.circ_failed + self.circ_succeeded + self.circ_suspected
+      + self.circ_uncounted != self.circ_chosen):
+      plog("ERROR", self.nickname+" does not add up for circs")
+    if (self.strm_failed + self.strm_succeeded + self.strm_suspected
+      + self.strm_uncounted != self.strm_chosen):
+      plog("ERROR", self.nickname+" does not add up for streams")
+    def check_reasons(reasons, expected, which, rtype):
+      count = 0
+      for rs in reasons.iterkeys():
+        if re.search(r"^"+which, rs): count += reasons[rs]
+      if count != expected:
+        plog("ERROR", "Mismatch "+which+" "+rtype+" for "+self.nickname)
+    check_reasons(self.reason_suspected,self.strm_suspected,"STREAM","susp")
+    check_reasons(self.reason_suspected,self.circ_suspected,"CIRC","susp")
+    check_reasons(self.reason_failed,self.strm_failed,"STREAM","failed")
+    check_reasons(self.reason_failed,self.circ_failed,"CIRC","failed")
+    now = time.time()
+    tot_hib_time = self.total_hibernation_time
+    tot_uptime = self.total_active_uptime
+    if self.hibernated_at: tot_hib_time += now - self.hibernated_at
+    if self.became_active_at: tot_uptime += now - self.became_active_at
+    if round(tot_hib_time+tot_uptime) != round(now-self.first_seen):
+      plog("ERROR", "Mismatch of uptimes for "+self.nickname)
+    
+    per_hour_tot = round(self._uncounted_per_hour()+self.failed_per_hour()+
+         self._suspected_per_hour()+self._succeeded_per_hour(), 2)
+    chosen_tot = round(self._chosen_per_hour(), 2)
+    if per_hour_tot != chosen_tot:
+      plog("ERROR", self.nickname+" has mismatch of per hour counts: "
+                    +str(per_hour_tot) +" vs "+str(chosen_tot))
+
+
+# TODO: Use __metaclass__ and type to make this inheritance flexible?
+class StatsHandler(PathSupport.PathBuilder):
+  """An extension of PathSupport.PathBuilder that keeps track of 
+     router statistics for every circuit and stream"""
+  def __init__(self, c, slmgr, RouterClass=StatsRouter, track_ranks=False):
+    PathBuilder.__init__(self, c, slmgr, RouterClass)
+    self.circ_count = 0
+    self.strm_count = 0
+    self.strm_failed = 0
+    self.circ_failed = 0
+    self.circ_succeeded = 0
+    self.failed_reasons = {}
+    self.suspect_reasons = {}
+    self.track_ranks = track_ranks
+
+  # XXX: Shit, all this stuff should be slice-based
+  def run_zbtest(self): # Unweighted z-test
+    """Run unweighted z-test to calculate the probabilities of a node
+       having a given stream bandwidth based on the Normal distribution"""
+    n = reduce(lambda x, y: x+(y.bwstats.mean > 0), self.sorted_r, 0)
+    if n == 0: return (0, 0)
+    avg = reduce(lambda x, y: x+y.bwstats.mean, self.sorted_r, 0)/float(n)
+    def notlambda(x, y):
+      if y.bwstats.mean <= 0: return x+0
+      else: return x+(y.bwstats.mean-avg)*(y.bwstats.mean-avg)
+    stddev = math.sqrt(reduce(notlambda, self.sorted_r, 0)/float(n))
+    if not stddev: return (avg, stddev)
+    for r in self.sorted_r:
+      if r.bwstats.mean > 0:
+        r.z_bw = abs((r.bwstats.mean-avg)/stddev)
+        r.prob_zb = TorUtil.zprob(-r.z_bw)
+    return (avg, stddev)
+
+  def run_zrtest(self): # Unweighted z-test
+    """Run unweighted z-test to calculate the probabilities of a node
+       having a given ratio of stream bandwidth to advertised bandwidth
+       based on the Normal distribution"""
+    n = reduce(lambda x, y: x+(y.bw_ratio() > 0), self.sorted_r, 0)
+    if n == 0: return (0, 0)
+    avg = reduce(lambda x, y: x+y.bw_ratio(), self.sorted_r, 0)/float(n)
+    def notlambda(x, y):
+      if y.bw_ratio() <= 0: return x+0
+      else: return x+(y.bw_ratio()-avg)*(y.bw_ratio()-avg)
+    stddev = math.sqrt(reduce(notlambda, self.sorted_r, 0)/float(n))
+    if not stddev: return (avg, stddev)
+    for r in self.sorted_r:
+      if r.bw_ratio() > 0:
+        r.z_ratio = abs((r.bw_ratio()-avg)/stddev)
+        r.prob_zr = TorUtil.zprob(-r.z_ratio)
+    return (avg, stddev)
+
+  def avg_adv_bw(self):
+    n = reduce(lambda x, y: x+y.was_used(), self.sorted_r, 0)
+    if n == 0: return (0, 0)
+    avg = reduce(lambda x, y: x+y.bw, 
+            filter(lambda r: r.was_used(), self.sorted_r), 0)/float(n)
+    return avg 
+
+  def avg_circ_failure(self):
+    n = reduce(lambda x, y: x+y.was_used(), self.sorted_r, 0)
+    if n == 0: return (0, 0)
+    avg = reduce(lambda x, y: x+y.circ_fail_rate(), 
+            filter(lambda r: r.was_used(), self.sorted_r), 0)/float(n)
+    return avg 
+
+  def avg_stream_failure(self):
+    n = reduce(lambda x, y: x+y.was_used(), self.sorted_r, 0)
+    if n == 0: return (0, 0)
+    avg = reduce(lambda x, y: x+y.strm_fail_rate(), 
+            filter(lambda r: r.was_used(), self.sorted_r), 0)/float(n)
+    return avg 
+
+  def avg_circ_suspects(self):
+    n = reduce(lambda x, y: x+y.was_used(), self.sorted_r, 0)
+    if n == 0: return (0, 0)
+    avg = reduce(lambda x, y: x+y.circ_suspect_rate(), 
+            filter(lambda r: r.was_used(), self.sorted_r), 0)/float(n)
+    return avg 
+
+  def avg_stream_suspects(self):
+    n = reduce(lambda x, y: x+y.was_used(), self.sorted_r, 0)
+    if n == 0: return (0, 0)
+    avg = reduce(lambda x, y: x+y.strm_suspect_rate(), 
+            filter(lambda r: r.was_used(), self.sorted_r), 0)/float(n)
+    return avg 
+
+  def write_reasons(self, f, reasons, name):
+    "Write out all the failure reasons and statistics for all Routers"
+    f.write("\n\n\t----------------- "+name+" -----------------\n")
+    for rsn in reasons:
+      f.write("\n"+rsn.reason+". Failed: "+str(rsn.total_failed())
+          +", Suspected: "+str(rsn.total_suspected())+"\n")
+      rsn.write_list(f)
+
+  def write_routers(self, f, rlist, name):
+    "Write out all the usage statistics for all Routers"
+    f.write("\n\n\t----------------- "+name+" -----------------\n\n")
+    for r in rlist:
+      # only print it if we've used it.
+      if r.circ_chosen+r.strm_chosen > 0: f.write(str(r))
+
+  # FIXME: Maybe move this two up into StatsRouter too?
+  ratio_key = """Metatroller Ratio Statistics:
+  SR=Stream avg ratio     AR=Advertised bw ratio    BRR=Adv. bw avg ratio
+  CSR=Circ suspect ratio  CFR=Circ Fail Ratio       SSR=Stream suspect ratio
+  SFR=Stream fail ratio   CC=Circuit Count          SC=Stream Count
+  P=Percentile Rank       U=Uptime (h)\n"""
+  
+  def write_ratios(self, filename):
+    "Write out bandwith ratio stats StatsHandler has gathered"
+    plog("DEBUG", "Writing ratios to "+filename)
+    f = file(filename, "w")
+    f.write(StatsHandler.ratio_key)
+
+    (avg, dev) = self.run_zbtest()
+    StatsRouter.global_strm_mean = avg
+    StatsRouter.global_strm_dev = dev
+    (avg, dev) = self.run_zrtest()
+    StatsRouter.global_ratio_mean = avg
+    StatsRouter.global_ratio_dev = dev
+
+    StatsRouter.global_bw_mean = self.avg_adv_bw()
+
+    StatsRouter.global_cf_mean = self.avg_circ_failure()
+    StatsRouter.global_sf_mean = self.avg_stream_failure()
+    
+    StatsRouter.global_cs_mean = self.avg_circ_suspects()
+    StatsRouter.global_ss_mean = self.avg_stream_suspects()
+
+    strm_bw_ratio = copy.copy(self.sorted_r)
+    strm_bw_ratio.sort(lambda x, y: cmp(x.strm_bw_ratio(), y.strm_bw_ratio()))
+    for r in strm_bw_ratio:
+      if r.circ_chosen == 0: continue
+      f.write(r.idhex+"="+r.nickname+"\n  ")
+      f.write("SR="+str(round(r.strm_bw_ratio(),4))+" AR="+str(round(r.adv_ratio(), 4))+" BRR="+str(round(r.bw_ratio_ratio(),4))+" CSR="+str(round(r.circ_suspect_ratio(),4))+" CFR="+str(round(r.circ_fail_ratio(),4))+" SSR="+str(round(r.strm_suspect_ratio(),4))+" SFR="+str(round(r.strm_fail_ratio(),4))+" CC="+str(r.circ_chosen)+" SC="+str(r.strm_chosen)+" U="+str(round(r.current_uptime()/3600,1))+" P="+str(round((100.0*r.avg_rank())/len(self.sorted_r),1))+"\n")
+    f.close()
+ 
+  def write_stats(self, filename):
+    "Write out all the statistics the StatsHandler has gathered"
+    # TODO: all this shit should be configurable. Some of it only makes
+    # sense when scanning in certain modes.
+    plog("DEBUG", "Writing stats to "+filename)
+    # Sanity check routers
+    for r in self.sorted_r: r.sanity_check()
+
+    # Sanity check the router reason lists.
+    for r in self.sorted_r:
+      for rsn in r.reason_failed:
+        if rsn not in self.failed_reasons:
+          plog("ERROR", "Router "+r.idhex+" w/o reason "+rsn+" in fail table")
+        elif r not in self.failed_reasons[rsn].rlist:
+          plog("ERROR", "Router "+r.idhex+" missing from fail table")
+      for rsn in r.reason_suspected:
+        if rsn not in self.suspect_reasons:
+          plog("ERROR", "Router "+r.idhex+" w/o reason "+rsn+" in fail table") 
+        elif r not in self.suspect_reasons[rsn].rlist:
+          plog("ERROR", "Router "+r.idhex+" missing from suspect table")
+
+    # Sanity check the lists the other way
+    for rsn in self.failed_reasons.itervalues(): rsn._verify_failed()
+    for rsn in self.suspect_reasons.itervalues(): rsn._verify_suspected()
+
+    f = file(filename, "w")
+    f.write(StatsRouter.key)
+    (avg, dev) = self.run_zbtest()
+    StatsRouter.global_strm_mean = avg
+    StatsRouter.global_strm_dev = dev
+    f.write("\n\nBW stats: u="+str(round(avg,1))+" s="+str(round(dev,1))+"\n")
+
+    (avg, dev) = self.run_zrtest()
+    StatsRouter.global_ratio_mean = avg
+    StatsRouter.global_ratio_dev = dev
+    f.write("BW ratio stats: u="+str(round(avg,1))+" s="+str(round(dev,1))+"\n")
+
+
+    # Circ, strm infoz
+    f.write("Circ failure ratio: "+str(self.circ_failed)
+            +"/"+str(self.circ_count)+"\n")
+
+    f.write("Stream failure ratio: "+str(self.strm_failed)
+            +"/"+str(self.strm_count)+"\n")
+
+    # Extend times 
+    n = 0.01+reduce(lambda x, y: x+(y.avg_extend_time() > 0), self.sorted_r, 0)
+    avg_extend = reduce(lambda x, y: x+y.avg_extend_time(), self.sorted_r, 0)/n
+    def notlambda(x, y):
+      return x+(y.avg_extend_time()-avg_extend)*(y.avg_extend_time()-avg_extend) 
+    dev_extend = math.sqrt(reduce(notlambda, self.sorted_r, 0)/float(n))
+
+    f.write("Extend time: u="+str(round(avg_extend,1))
+             +" s="+str(round(dev_extend,1)))
+    
+    # sort+print by bandwidth
+    strm_bw_ratio = copy.copy(self.sorted_r)
+    strm_bw_ratio.sort(lambda x, y: cmp(x.strm_bw_ratio(), y.strm_bw_ratio()))
+    self.write_routers(f, strm_bw_ratio, "Stream Ratios")
+
+    # sort+print by bandwidth
+    bw_rate = copy.copy(self.sorted_r)
+    bw_rate.sort(lambda x, y: cmp(y.bw_ratio(), x.bw_ratio()))
+    self.write_routers(f, bw_rate, "Bandwidth Ratios")
+
+    failed = copy.copy(self.sorted_r)
+    failed.sort(lambda x, y:
+          cmp(y.circ_failed+y.strm_failed,
+            x.circ_failed+x.strm_failed))
+    self.write_routers(f, failed, "Failed Counts")
+
+    suspected = copy.copy(self.sorted_r)
+    suspected.sort(lambda x, y: # Suspected includes failed
+       cmp(y.circ_failed+y.strm_failed+y.circ_suspected+y.strm_suspected,
+         x.circ_failed+x.strm_failed+x.circ_suspected+x.strm_suspected))
+    self.write_routers(f, suspected, "Suspected Counts")
+
+    fail_rate = copy.copy(failed)
+    fail_rate.sort(lambda x, y: cmp(y.failed_per_hour(), x.failed_per_hour()))
+    self.write_routers(f, fail_rate, "Fail Rates")
+
+    suspect_rate = copy.copy(suspected)
+    suspect_rate.sort(lambda x, y:
+       cmp(y.suspected_per_hour(), x.suspected_per_hour()))
+    self.write_routers(f, suspect_rate, "Suspect Rates")
+    
+    # TODO: Sort by failed/selected and suspect/selected ratios
+    # if we ever want to do non-uniform scanning..
+
+    # FIXME: Add failed in here somehow..
+    susp_reasons = self.suspect_reasons.values()
+    susp_reasons.sort(lambda x, y:
+       cmp(y.total_suspected(), x.total_suspected()))
+    self.write_reasons(f, susp_reasons, "Suspect Reasons")
+
+    fail_reasons = self.failed_reasons.values()
+    fail_reasons.sort(lambda x, y:
+       cmp(y.total_failed(), x.total_failed()))
+    self.write_reasons(f, fail_reasons, "Failed Reasons")
+    f.close()
+
+    # FIXME: sort+print by circ extend time
+
+  def reset(self):
+    PathSupport.PathBuilder.reset(self)
+    self.reset_stats()
+
+  def reset_stats(self):
+    plog("DEBUG", "Resetting stats")
+    self.circ_count = 0
+    self.strm_count = 0
+    self.strm_failed = 0
+    self.circ_succeeded = 0
+    self.circ_failed = 0
+    self.suspect_reasons.clear()
+    self.failed_reasons.clear()
+    for r in self.routers.itervalues(): r.reset()
+
+  def close_circuit(self, id):
+    PathSupport.PathBuilder.close_circuit(self, id)
+    # Shortcut so we don't have to wait for the CLOSE
+    # events for stats update.
+    self.circ_succeeded += 1
+    for r in self.circuits[id].path:
+      r.circ_chosen += 1
+      r.circ_succeeded += 1
+
+  def circ_status_event(self, c):
+    if c.circ_id in self.circuits:
+      # TODO: Hrmm, consider making this sane in TorCtl.
+      if c.reason: lreason = c.reason
+      else: lreason = "NONE"
+      if c.remote_reason: rreason = c.remote_reason
+      else: rreason = "NONE"
+      reason = c.event_name+":"+c.status+":"+lreason+":"+rreason
+      if c.status == "LAUNCHED":
+        # Update circ_chosen count
+        self.circ_count += 1
+      elif c.status == "EXTENDED":
+        delta = c.arrived_at - self.circuits[c.circ_id].last_extended_at
+        r_ext = c.path[-1]
+        try:
+          if r_ext[0] != '$': r_ext = self.name_to_key[r_ext]
+          self.routers[r_ext[1:]].total_extend_time += delta
+          self.routers[r_ext[1:]].total_extended += 1
+        except KeyError, e:
+          traceback.print_exc()
+          plog("WARN", "No key "+str(e)+" for "+r_ext+" in dict:"+str(self.name_to_key))
+      elif c.status == "FAILED":
+        for r in self.circuits[c.circ_id].path: r.circ_chosen += 1
+        
+        if len(c.path)-1 < 0: start_f = 0
+        else: start_f = len(c.path)-1 
+
+        # Count failed
+        self.circ_failed += 1
+        # XXX: Differentiate between extender and extendee
+        for r in self.circuits[c.circ_id].path[start_f:len(c.path)+1]:
+          r.circ_failed += 1
+          if not reason in r.reason_failed:
+            r.reason_failed[reason] = 1
+          else: r.reason_failed[reason]+=1
+          if reason not in self.failed_reasons:
+             self.failed_reasons[reason] = FailedRouterList(reason)
+          self.failed_reasons[reason].add_r(r)
+
+        for r in self.circuits[c.circ_id].path[len(c.path)+1:]:
+          r.circ_uncounted += 1
+
+        # Don't count if failed was set this round, don't set 
+        # suspected..
+        for r in self.circuits[c.circ_id].path[:start_f]:
+          r.circ_suspected += 1
+          if not reason in r.reason_suspected:
+            r.reason_suspected[reason] = 1
+          else: r.reason_suspected[reason]+=1
+          if reason not in self.suspect_reasons:
+             self.suspect_reasons[reason] = SuspectRouterList(reason)
+          self.suspect_reasons[reason].add_r(r)
+      elif c.status == "CLOSED":
+        # Since PathBuilder deletes the circuit on a failed, 
+        # we only get this for a clean close that was not
+        # requested by us.
+
+        # Don't count circuits we requested closed from
+        # pathbuilder, they are counted there instead.
+        if not self.circuits[c.circ_id].requested_closed:
+          self.circ_succeeded += 1
+          for r in self.circuits[c.circ_id].path:
+            r.circ_chosen += 1
+            if lreason in ("REQUESTED", "FINISHED", "ORIGIN"):
+              r.circ_succeeded += 1
+            else:
+              if not reason in r.reason_suspected:
+                r.reason_suspected[reason] = 1
+              else: r.reason_suspected[reason] += 1
+              r.circ_suspected+= 1
+              if reason not in self.suspect_reasons:
+                self.suspect_reasons[reason] = SuspectRouterList(reason)
+              self.suspect_reasons[reason].add_r(r)
+    PathBuilder.circ_status_event(self, c)
+
+  def count_stream_reason_failed(self, s, reason):
+    "Count the routers involved in a failure"
+    # Update failed count,reason_failed for exit
+    r = self.circuits[s.circ_id].exit
+    if not reason in r.reason_failed: r.reason_failed[reason] = 1
+    else: r.reason_failed[reason]+=1
+    r.strm_failed += 1
+    if reason not in self.failed_reasons:
+      self.failed_reasons[reason] = FailedRouterList(reason)
+    self.failed_reasons[reason].add_r(r)
+
+  def count_stream_suspects(self, s, lreason, reason):
+    "Count the routers 'suspected' of being involved in a failure"
+    if lreason in ("TIMEOUT", "INTERNAL", "TORPROTOCOL" "DESTROY"):
+      for r in self.circuits[s.circ_id].path[:-1]:
+        r.strm_suspected += 1
+        if not reason in r.reason_suspected:
+          r.reason_suspected[reason] = 1
+        else: r.reason_suspected[reason]+=1
+        if reason not in self.suspect_reasons:
+          self.suspect_reasons[reason] = SuspectRouterList(reason)
+        self.suspect_reasons[reason].add_r(r)
+    else:
+      for r in self.circuits[s.circ_id].path[:-1]:
+        r.strm_uncounted += 1
+  
+  def stream_status_event(self, s):
+    if s.strm_id in self.streams and not self.streams[s.strm_id].ignored:
+      # TODO: Hrmm, consider making this sane in TorCtl.
+      if s.reason: lreason = s.reason
+      else: lreason = "NONE"
+      if s.remote_reason: rreason = s.remote_reason
+      else: rreason = "NONE"
+      reason = s.event_name+":"+s.status+":"+lreason+":"+rreason+":"+self.streams[s.strm_id].kind
+      circ = self.streams[s.strm_id].circ
+      if not circ: circ = self.streams[s.strm_id].pending_circ
+      if (s.status in ("DETACHED", "FAILED", "CLOSED", "SUCCEEDED")
+          and not s.circ_id):
+        # XXX: REMAPs can do this (normal). Also REASON=DESTROY (bug?)
+        if circ:
+          plog("INFO", "Stream "+s.status+" of "+str(s.strm_id)+" gave circ 0.  Resetting to stored circ id: "+str(circ.circ_id))
+          s.circ_id = circ.circ_id
+        #elif s.reason == "TIMEOUT" or s.reason == "EXITPOLICY":
+        #  plog("NOTICE", "Stream "+str(s.strm_id)+" detached with "+s.reason)
+        else:
+          plog("WARN", "Stream "+str(s.strm_id)+" detached from no known circuit with reason: "+str(s.reason))
+          PathBuilder.stream_status_event(self, s)
+          return
+
+      # Verify circ id matches stream.circ
+      if s.status not in ("NEW", "NEWRESOLVE", "REMAP"):
+        if s.circ_id and circ and circ.circ_id != s.circ_id:
+          plog("WARN", str(s.strm_id) + " has mismatch of "
+                +str(s.circ_id)+" v "+str(circ.circ_id))
+        if s.circ_id and s.circ_id not in self.circuits:
+          plog("NOTICE", "Unknown circuit "+str(s.circ_id)
+                +" for stream "+str(s.strm_id))
+          PathBuilder.stream_status_event(self, s)
+          return
+      
+      if s.status == "DETACHED":
+        if self.streams[s.strm_id].attached_at:
+          plog("WARN", str(s.strm_id)+" detached after succeeded")
+        # Update strm_chosen count
+        self.strm_count += 1
+        for r in self.circuits[s.circ_id].path: r.strm_chosen += 1
+        self.strm_failed += 1
+        self.count_stream_suspects(s, lreason, reason)
+        self.count_stream_reason_failed(s, reason)
+      elif s.status == "FAILED":
+        # HACK. We get both failed and closed for the same stream,
+        # with different reasons. Might as well record both, since they 
+        # often differ.
+        self.streams[s.strm_id].failed_reason = reason
+      elif s.status == "CLOSED":
+        # Always get both a closed and a failed.. 
+        #   - Check if the circuit exists still
+        # Update strm_chosen count
+        self.strm_count += 1
+        for r in self.circuits[s.circ_id].path: r.strm_chosen += 1
+
+        if self.streams[s.strm_id].failed:
+          reason = self.streams[s.strm_id].failed_reason+":"+lreason+":"+rreason
+
+        self.count_stream_suspects(s, lreason, reason)
+          
+        r = self.circuits[s.circ_id].exit
+        if (not self.streams[s.strm_id].failed
+          and (lreason == "DONE" or (lreason == "END" and rreason == "DONE"))):
+          r.strm_succeeded += 1
+
+          # Update bw stats. XXX: Don't do this for resolve streams
+          if self.streams[s.strm_id].attached_at:
+            lifespan = self.streams[s.strm_id].lifespan(s.arrived_at)
+            for r in self.streams[s.strm_id].circ.path:
+              r.bwstats.add_bw(self.streams[s.strm_id].bytes_written+
+                               self.streams[s.strm_id].bytes_read, lifespan)
+  
+        else:
+          self.strm_failed += 1
+          self.count_stream_reason_failed(s, reason)
+    PathBuilder.stream_status_event(self, s)
+
+  def _check_hibernation(self, r, now):
+    if r.down:
+      if not r.hibernated_at:
+        r.hibernated_at = now
+        r.total_active_uptime += now - r.became_active_at
+      r.became_active_at = 0
+    else:
+      if not r.became_active_at:
+        r.became_active_at = now
+        r.total_hibernation_time += now - r.hibernated_at
+      r.hibernated_at = 0
+
+  def new_consensus_event(self, n):
+    if self.track_ranks:
+      # Record previous rank and history.
+      for ns in n.nslist:
+        if not ns.idhex in self.routers:
+          continue
+        r = self.routers[ns.idhex]
+        r.bw_history.append(r.bw)
+      for r in self.sorted_r:
+        r.rank_history.append(r.list_rank)
+    PathBuilder.new_consensus_event(self, n)
+    now = n.arrived_at
+    for ns in n.nslist:
+      if not ns.idhex in self.routers: continue
+      self._check_hibernation(self.routers[ns.idhex], now)
+
+  def new_desc_event(self, d):
+    if PathBuilder.new_desc_event(self, d):
+      now = d.arrived_at
+      for i in d.idlist:
+        if not i in self.routers: continue
+        self._check_hibernation(self.routers[i], now)
+      
+   

Added: arm/dependencies/TorCtl/TorCtl.py
===================================================================
--- arm/dependencies/TorCtl/TorCtl.py	                        (rev 0)
+++ arm/dependencies/TorCtl/TorCtl.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,1645 @@
+#!/usr/bin/python
+# TorCtl.py -- Python module to interface with Tor Control interface.
+# Copyright 2005 Nick Mathewson
+# Copyright 2007 Mike Perry. See LICENSE file.
+
+"""
+Library to control Tor processes.
+
+This library handles sending commands, parsing responses, and delivering
+events to and from the control port. The basic usage is to create a
+socket, wrap that in a TorCtl.Connection, and then add an EventHandler
+to that connection. A simple example with a DebugEventHandler (that just
+echoes the events back to stdout) is present in run_example().
+
+Note that the TorCtl.Connection is fully compatible with the more
+advanced EventHandlers in TorCtl.PathSupport (and of course any other
+custom event handlers that you may extend off of those).
+
+This package also contains a helper class for representing Routers, and
+classes and constants for each event.
+
+"""
+
+__all__ = ["EVENT_TYPE", "TorCtlError", "TorCtlClosed", "ProtocolError",
+           "ErrorReply", "NetworkStatus", "ExitPolicyLine", "Router",
+           "RouterVersion", "Connection", "parse_ns_body",
+           "EventHandler", "DebugEventHandler", "NetworkStatusEvent",
+           "NewDescEvent", "CircuitEvent", "StreamEvent", "ORConnEvent",
+           "StreamBwEvent", "LogEvent", "AddrMapEvent", "BWEvent",
+           "BuildTimeoutSetEvent", "UnknownEvent", "ConsensusTracker",
+           "EventListener", "EVENT_STATE" ]
+
+import os
+import re
+import struct
+import sys
+import threading
+import Queue
+import datetime
+import traceback
+import socket
+import binascii
+import types
+import time
+import copy
+
+from TorUtil import *
+
+if sys.version_info < (2, 5):
+  from sets import Set as set
+  from sha import sha as sha1
+else:
+  from hashlib import sha1
+
+# Types of "EVENT" message.
+EVENT_TYPE = Enum2(
+          CIRC="CIRC",
+          STREAM="STREAM",
+          ORCONN="ORCONN",
+          STREAM_BW="STREAM_BW",
+          BW="BW",
+          NS="NS",
+          NEWCONSENSUS="NEWCONSENSUS",
+          BUILDTIMEOUT_SET="BUILDTIMEOUT_SET",
+          GUARD="GUARD",
+          NEWDESC="NEWDESC",
+          ADDRMAP="ADDRMAP",
+          DEBUG="DEBUG",
+          INFO="INFO",
+          NOTICE="NOTICE",
+          WARN="WARN",
+          ERR="ERR")
+
+EVENT_STATE = Enum2(
+          PRISTINE="PRISTINE",
+          PRELISTEN="PRELISTEN",
+          HEARTBEAT="HEARTBEAT",
+          HANDLING="HANDLING",
+          POSTLISTEN="POSTLISTEN",
+          DONE="DONE")
+
+class TorCtlError(Exception):
+  "Generic error raised by TorControl code."
+  pass
+
+class TorCtlClosed(TorCtlError):
+  "Raised when the controller connection is closed by Tor (not by us.)"
+  pass
+
+class ProtocolError(TorCtlError):
+  "Raised on violations in Tor controller protocol"
+  pass
+
+class ErrorReply(TorCtlError):
+  "Raised when Tor controller returns an error"
+  def __init__(self, *args, **kwargs):
+    if "status" in kwargs:
+      self.status = kwargs.pop("status")
+    if "message" in kwargs:
+      self.message = kwargs.pop("message")
+    TorCtlError.__init__(self, *args, **kwargs)
+
+class NetworkStatus:
+  "Filled in during NS events"
+  def __init__(self, nickname, idhash, orhash, updated, ip, orport, dirport, flags, bandwidth=None):
+    self.nickname = nickname
+    self.idhash = idhash
+    self.orhash = orhash
+    self.ip = ip
+    self.orport = int(orport)
+    self.dirport = int(dirport)
+    self.flags = flags
+    self.idhex = (self.idhash + "=").decode("base64").encode("hex").upper()
+    self.bandwidth = bandwidth
+    m = re.search(r"(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)", updated)
+    self.updated = datetime.datetime(*map(int, m.groups()))
+
+class Event:
+  def __init__(self, event_name):
+    self.event_name = event_name
+    self.arrived_at = 0
+    self.state = EVENT_STATE.PRISTINE
+
+class TimerEvent(Event):
+  def __init__(self, event_name, type):
+    Event.__init__(self, event_name)
+    self.type = type
+
+class NetworkStatusEvent(Event):
+  def __init__(self, event_name, nslist):
+    Event.__init__(self, event_name)
+    self.nslist = nslist # List of NetworkStatus objects
+
+class NewConsensusEvent(NetworkStatusEvent):
+  pass
+
+class NewDescEvent(Event):
+  def __init__(self, event_name, idlist):
+    Event.__init__(self, event_name)
+    self.idlist = idlist
+
+class GuardEvent(Event):
+  def __init__(self, event_name, ev_type, guard, status):
+    Event.__init__(self, event_name)
+    if "~" in guard:
+      (self.idhex, self.nick) = guard[1:].split("~")
+    elif "=" in guard:
+      (self.idhex, self.nick) = guard[1:].split("=")
+    else:
+      self.idhex = guard[1:]
+    self.status = status
+
+class BuildTimeoutSetEvent(Event):
+  def __init__(self, event_name, set_type, total_times, timeout_ms, xm, alpha,
+               quantile):
+    Event.__init__(self, event_name)
+    self.set_type = set_type
+    self.total_times = total_times
+    self.timeout_ms = timeout_ms
+    self.xm = xm
+    self.alpha = alpha
+    self.cutoff_quantile = quantile
+
+class CircuitEvent(Event):
+  def __init__(self, event_name, circ_id, status, path, purpose,
+         reason, remote_reason):
+    Event.__init__(self, event_name)
+    self.circ_id = circ_id
+    self.status = status
+    self.path = path
+    self.purpose = purpose
+    self.reason = reason
+    self.remote_reason = remote_reason
+
+class StreamEvent(Event):
+  def __init__(self, event_name, strm_id, status, circ_id, target_host,
+         target_port, reason, remote_reason, source, source_addr, purpose):
+    Event.__init__(self, event_name)
+    self.strm_id = strm_id
+    self.status = status
+    self.circ_id = circ_id
+    self.target_host = target_host
+    self.target_port = int(target_port)
+    self.reason = reason
+    self.remote_reason = remote_reason
+    self.source = source
+    self.source_addr = source_addr
+    self.purpose = purpose
+
+class ORConnEvent(Event):
+  def __init__(self, event_name, status, endpoint, age, read_bytes,
+         wrote_bytes, reason, ncircs):
+    Event.__init__(self, event_name)
+    self.status = status
+    self.endpoint = endpoint
+    self.age = age
+    self.read_bytes = read_bytes
+    self.wrote_bytes = wrote_bytes
+    self.reason = reason
+    self.ncircs = ncircs
+
+class StreamBwEvent(Event):
+  def __init__(self, event_name, strm_id, written, read):
+    Event.__init__(self, event_name)
+    self.strm_id = int(strm_id)
+    self.bytes_read = int(read)
+    self.bytes_written = int(written)
+
+class LogEvent(Event):
+  def __init__(self, level, msg):
+    Event.__init__(self, level)
+    self.level = level
+    self.msg = msg
+
+class AddrMapEvent(Event):
+  def __init__(self, event_name, from_addr, to_addr, when):
+    Event.__init__(self, event_name)
+    self.from_addr = from_addr
+    self.to_addr = to_addr
+    self.when = when
+
+class AddrMap:
+  def __init__(self, from_addr, to_addr, when):
+    self.from_addr = from_addr
+    self.to_addr = to_addr
+    self.when = when
+
+class BWEvent(Event):
+  def __init__(self, event_name, read, written):
+    Event.__init__(self, event_name)
+    self.read = read
+    self.written = written
+
+class UnknownEvent(Event):
+  def __init__(self, event_name, event_string):
+    Event.__init__(self, event_name)
+    self.event_string = event_string
+
+ipaddress_re = re.compile(r"(\d{1,3}\.){3}\d{1,3}$")
+class ExitPolicyLine:
+  """ Class to represent a line in a Router's exit policy in a way 
+      that can be easily checked. """
+  def __init__(self, match, ip_mask, port_low, port_high):
+    self.match = match
+    if ip_mask == "*":
+      self.ip = 0
+      self.netmask = 0
+    else:
+      if not "/" in ip_mask:
+        self.netmask = 0xFFFFFFFF
+        ip = ip_mask
+      else:
+        ip, mask = ip_mask.split("/")
+        if ipaddress_re.match(mask):
+          self.netmask=struct.unpack(">I", socket.inet_aton(mask))[0]
+        else:
+          self.netmask = 0xffffffff ^ (0xffffffff >> int(mask))
+      self.ip = struct.unpack(">I", socket.inet_aton(ip))[0]
+    self.ip &= self.netmask
+    if port_low == "*":
+      self.port_low,self.port_high = (0,65535)
+    else:
+      if not port_high:
+        port_high = port_low
+      self.port_low = int(port_low)
+      self.port_high = int(port_high)
+  
+  def check(self, ip, port):
+    """Check to see if an ip and port is matched by this line. 
+     Returns true if the line is an Accept, and False if it is a Reject. """
+    ip = struct.unpack(">I", socket.inet_aton(ip))[0]
+    if (ip & self.netmask) == self.ip:
+      if self.port_low <= port and port <= self.port_high:
+        return self.match
+    return -1
+
+  def __str__(self):
+    retr = ""
+    if self.match:
+      retr += "accept "
+    else:
+      retr += "reject "
+    retr += socket.inet_ntoa(struct.pack(">I",self.ip)) + "/"
+    retr += socket.inet_ntoa(struct.pack(">I",self.netmask)) + ":"
+    retr += str(self.port_low)+"-"+str(self.port_high)
+    return retr
+
+class RouterVersion:
+  """ Represents a Router's version. Overloads all comparison operators
+      to check for newer, older, or equivalent versions. """
+  def __init__(self, version):
+    if version:
+      v = re.search("^(\d+)\.(\d+)\.(\d+)\.(\d+)", version).groups()
+      self.version = int(v[0])*0x1000000 + int(v[1])*0x10000 + int(v[2])*0x100 + int(v[3])
+      self.ver_string = version
+    else: 
+      self.version = version
+      self.ver_string = "unknown"
+
+  def __lt__(self, other): return self.version < other.version
+  def __gt__(self, other): return self.version > other.version
+  def __ge__(self, other): return self.version >= other.version
+  def __le__(self, other): return self.version <= other.version
+  def __eq__(self, other): return self.version == other.version
+  def __ne__(self, other): return self.version != other.version
+  def __str__(self): return self.ver_string
+
+
+# map descriptor keywords to regular expressions.
+desc_re = {
+  "router":          r"(\S+) (\S+)",
+  "opt fingerprint": r"(.+).*on (\S+)",
+  "opt hibernating": r"1$",
+  "platform":  r"Tor (\S+).*on ([\S\s]+)",
+  "accept":    r"(\S+):([^-]+)(?:-(\d+))?",
+  "reject":    r"(\S+):([^-]+)(?:-(\d+))?",
+  "bandwidth": r"(\d+) \d+ (\d+)",
+  "uptime":    r"(\d+)",
+  "contact":   r"(.+)",
+  "published": r"(\S+ \S+)",
+}
+# Compile each regular expression now.
+for kw, reg in desc_re.iteritems():
+  desc_re[kw] = re.compile(reg)
+
+class Router:
+  """ 
+  Class to represent a router from a descriptor. Can either be
+  created from the parsed fields, or can be built from a
+  descriptor+NetworkStatus 
+  """     
+  def __init__(self, *args):
+    if len(args) == 1:
+      for i in args[0].__dict__:
+        self.__dict__[i] =  copy.deepcopy(args[0].__dict__[i])
+      return
+    else:
+      (idhex, name, bw, down, exitpolicy, flags, ip, version, os, uptime, published, contact, rate_limited, orhash, ns_bandwidth) = args
+    self.idhex = idhex
+    self.nickname = name
+    if ns_bandwidth != None:
+      self.bw = ns_bandwidth
+    else:
+     self.bw = bw
+    self.desc_bw = bw
+    self.exitpolicy = exitpolicy
+    self.flags = flags # Technicaly from NS doc
+    self.down = down
+    self.ip = struct.unpack(">I", socket.inet_aton(ip))[0]
+    self.version = RouterVersion(version)
+    self.os = os
+    self.list_rank = 0 # position in a sorted list of routers.
+    self.uptime = uptime
+    self.published = published
+    self.refcount = 0 # How many open circs are we currently in?
+    self.deleted = False # Has Tor already deleted this descriptor?
+    self.contact = contact
+    self.rate_limited = rate_limited
+    self.orhash = orhash
+    self._generated = [] # For ExactUniformGenerator
+
+  def __str__(self):
+    s = self.idhex, self.nickname
+    return s.__str__()
+
+  def build_from_desc(desc, ns):
+    """
+    Static method of Router that parses a descriptor string into this class.
+    'desc' is a full descriptor as a string. 
+    'ns' is a TorCtl.NetworkStatus instance for this router (needed for
+    the flags, the nickname, and the idhex string). 
+    Returns a Router instance.
+    """
+    exitpolicy = []
+    dead = not ("Running" in ns.flags)
+    bw_observed = 0
+    version = None
+    os = None
+    uptime = 0
+    ip = 0
+    router = "[none]"
+    published = "never"
+    contact = None
+
+    for line in desc:
+      # Pull off the keyword...
+      kw, _, rest = line.partition(" ")
+
+      # ...and if it's "opt", extend it by the next keyword
+      # so we get "opt hibernating" as one keyword.
+      if kw == "opt":
+        okw, _, rest = rest.partition(" ")
+        kw += " " + okw
+
+      # try to match the descriptor line by keyword.
+      try:
+        match = desc_re[kw].match(rest)
+      # if we don't handle this keyword, just move on to the next one.
+      except KeyError:
+        continue
+      # if we do handle this keyword but its data is malformed,
+      # move on to the next one without processing it.
+      if not match:
+        continue
+
+      g = match.groups()
+
+      # Handle each keyword individually.
+      # TODO: This could possibly be sped up since we technically already
+      # did the compare with the dictionary lookup... lambda magic time.
+      if kw == "accept":
+        exitpolicy.append(ExitPolicyLine(True, *g))
+      elif kw == "reject":
+        exitpolicy.append(ExitPolicyLine(False, *g))
+      elif kw == "router":
+        router,ip = g
+      elif kw == "bandwidth":
+        bws = map(int, g)
+        bw_observed = min(bws)
+        rate_limited = False
+        if bws[0] < bws[1]:
+          rate_limited = True
+      elif kw == "platform":
+        version, os = g
+      elif kw == "uptime":
+        uptime = int(g[0])
+      elif kw == "published":
+        t = time.strptime(g[0] + " UTC", "20%y-%m-%d %H:%M:%S %Z")
+        published = datetime.datetime(*t[0:6])
+      elif kw == "contact":
+        contact = g[0]
+      elif kw == "opt hibernating":
+        dead = True 
+        if ("Running" in ns.flags):
+          plog("INFO", "Hibernating router "+ns.nickname+" is running, flags: "+" ".join(ns.flags))
+
+    if router != ns.nickname:
+      plog("NOTICE", "Got different names " + ns.nickname + " vs " +
+             router + " for " + ns.idhex)
+    if not bw_observed and not dead and ("Valid" in ns.flags):
+      plog("INFO", "No bandwidth for live router "+ns.nickname+", flags: "+" ".join(ns.flags))
+      dead = True
+    if not version or not os:
+      plog("INFO", "No version and/or OS for router " + ns.nickname)
+    return Router(ns.idhex, ns.nickname, bw_observed, dead, exitpolicy,
+        ns.flags, ip, version, os, uptime, published, contact, rate_limited,
+        ns.orhash, ns.bandwidth)
+  build_from_desc = Callable(build_from_desc)
+
+  def update_to(self, new):
+    """ Somewhat hackish method to update this router to be a copy of
+    'new' """
+    if self.idhex != new.idhex:
+      plog("ERROR", "Update of router "+self.nickname+"changes idhex!")
+    plog("DEBUG", "Updating refcount "+str(self.refcount)+" for "+self.idhex)
+    for i in new.__dict__.iterkeys():
+      if i == "refcount" or i == "_generated": continue
+      self.__dict__[i] = new.__dict__[i]
+    plog("DEBUG", "Updated refcount "+str(self.refcount)+" for "+self.idhex)
+
+  def will_exit_to(self, ip, port):
+    """ Check the entire exitpolicy to see if the router will allow
+        connections to 'ip':'port' """
+    for line in self.exitpolicy:
+      ret = line.check(ip, port)
+      if ret != -1:
+        return ret
+    plog("WARN", "No matching exit line for "+self.nickname)
+    return False
+   
+class Connection:
+  """A Connection represents a connection to the Tor process via the 
+     control port."""
+  def __init__(self, sock):
+    """Create a Connection to communicate with the Tor process over the
+       socket 'sock'.
+    """
+    self._handler = None
+    self._handleFn = None
+    self._sendLock = threading.RLock()
+    self._queue = Queue.Queue()
+    self._thread = None
+    self._closedEx = None
+    self._closed = 0
+    self._closeHandler = None
+    self._eventThread = None
+    self._eventQueue = Queue.Queue()
+    self._s = BufSock(sock)
+    self._debugFile = None
+
+  def set_close_handler(self, handler):
+    """Call 'handler' when the Tor process has closed its connection or
+       given us an exception.  If we close normally, no arguments are
+       provided; otherwise, it will be called with an exception as its
+       argument.
+    """
+    self._closeHandler = handler
+
+  def close(self):
+    """Shut down this controller connection"""
+    self._sendLock.acquire()
+    try:
+      self._queue.put("CLOSE")
+      self._eventQueue.put((time.time(), "CLOSE"))
+      self._closed = 1
+      # XXX: For some reason, this does not cause the readline in
+      # self._read_reply() to return immediately. The _loop() thread
+      # thus tends to stick around until some event causes data to come
+      # back...
+      self._s.close()
+      self._eventThread.join()
+    finally:
+      self._sendLock.release()
+
+  def is_live(self):
+    """ Returns true iff the connection is alive and healthy"""
+    return self._thread.isAlive() and self._eventThread.isAlive() and not \
+           self._closed
+
+  def launch_thread(self, daemon=1):
+    """Launch a background thread to handle messages from the Tor process."""
+    assert self._thread is None
+    t = threading.Thread(target=self._loop, name="TorLoop")
+    if daemon:
+      t.setDaemon(daemon)
+    t.start()
+    self._thread = t
+    t = threading.Thread(target=self._eventLoop, name="EventLoop")
+    if daemon:
+      t.setDaemon(daemon)
+    t.start()
+    self._eventThread = t
+    # eventThread provides a more reliable indication of when we are done.
+    # The _loop thread won't always die when self.close() is called.
+    return self._eventThread
+
+  def _loop(self):
+    """Main subthread loop: Read commands from Tor, and handle them either
+       as events or as responses to other commands.
+    """
+    while 1:
+      try:
+        isEvent, reply = self._read_reply()
+      except TorCtlClosed:
+        plog("NOTICE", "Tor closed control connection. Exiting event thread.")
+        return
+      except Exception,e:
+        if not self._closed:
+          if sys:
+            self._err(sys.exc_info())
+          else:
+            plog("NOTICE", "No sys left at exception shutdown: "+str(e))
+            self._err((e.__class__, e, None))
+          return
+        else:
+          isEvent = 0
+
+      if isEvent:
+        if self._handler is not None:
+          self._eventQueue.put((time.time(), reply))
+      else:
+        cb = self._queue.get() # atomic..
+        if cb == "CLOSE":
+          self._s = None
+          plog("INFO", "Closed control connection. Exiting thread.")
+          return
+        else:
+          cb(reply)
+
+  def _err(self, (tp, ex, tb), fromEventLoop=0):
+    """DOCDOC"""
+    # silent death is bad :(
+    traceback.print_exception(tp, ex, tb)
+    if self._s:
+      try:
+        self.close()
+      except:
+        pass
+    self._sendLock.acquire()
+    try:
+      self._closedEx = ex
+      self._closed = 1
+    finally:
+      self._sendLock.release()
+    while 1:
+      try:
+        cb = self._queue.get(timeout=0)
+        if cb != "CLOSE":
+          cb("EXCEPTION")
+      except Queue.Empty:
+        break
+    if self._closeHandler is not None:
+      self._closeHandler(ex)
+    # I hate you for making me resort to this, python
+    os.kill(os.getpid(), 15)
+    return
+
+  def _eventLoop(self):
+    """DOCDOC"""
+    while 1:
+      (timestamp, reply) = self._eventQueue.get()
+      if reply[0][0] == "650" and reply[0][1] == "OK":
+        plog("DEBUG", "Ignoring incompatible syntactic sugar: 650 OK")
+        continue
+      if reply == "CLOSE":
+        plog("INFO", "Event loop received close message.")
+        return
+      try:
+        self._handleFn(timestamp, reply)
+      except:
+        for code, msg, data in reply:
+            plog("WARN", "No event for: "+str(code)+" "+str(msg))
+        self._err(sys.exc_info(), 1)
+        return
+
+  def _sendImpl(self, sendFn, msg):
+    """DOCDOC"""
+    if self._thread is None and not self._closed:
+      self.launch_thread(1)
+    # This condition will get notified when we've got a result...
+    condition = threading.Condition()
+    # Here's where the result goes...
+    result = []
+
+    if self._closedEx is not None:
+      raise self._closedEx
+    elif self._closed:
+      raise TorCtlClosed()
+
+    def cb(reply,condition=condition,result=result):
+      condition.acquire()
+      try:
+        result.append(reply)
+        condition.notify()
+      finally:
+        condition.release()
+
+    # Sends a message to Tor...
+    self._sendLock.acquire() # ensure queue+sendmsg is atomic
+    try:
+      self._queue.put(cb)
+      sendFn(msg) # _doSend(msg)
+    finally:
+      self._sendLock.release()
+
+    # Now wait till the answer is in...
+    condition.acquire()
+    try:
+      while not result:
+        condition.wait()
+    finally:
+      condition.release()
+
+    # ...And handle the answer appropriately.
+    assert len(result) == 1
+    reply = result[0]
+    if reply == "EXCEPTION":
+      raise self._closedEx
+
+    return reply
+
+
+  def debug(self, f):
+    """DOCDOC"""
+    self._debugFile = f
+
+  def set_event_handler(self, handler):
+    """Cause future events from the Tor process to be sent to 'handler'.
+    """
+    if self._handler:
+      handler.pre_listeners = self._handler.pre_listeners
+      handler.post_listeners = self._handler.post_listeners
+    self._handler = handler
+    self._handler.c = self
+    self._handleFn = handler._handle1
+
+  def add_event_listener(self, listener):
+    if not self._handler:
+      self.set_event_handler(EventHandler())
+    self._handler.add_event_listener(listener)
+
+  def _read_reply(self):
+    lines = []
+    while 1:
+      line = self._s.readline()
+      if not line:
+        self._closed = True
+        raise TorCtlClosed() 
+      line = line.strip()
+      if self._debugFile:
+        self._debugFile.write(str(time.time())+"\t  %s\n" % line)
+      if len(line)<4:
+        raise ProtocolError("Badly formatted reply line: Too short")
+      code = line[:3]
+      tp = line[3]
+      s = line[4:]
+      if tp == "-":
+        lines.append((code, s, None))
+      elif tp == " ":
+        lines.append((code, s, None))
+        isEvent = (lines and lines[0][0][0] == '6')
+        return isEvent, lines
+      elif tp != "+":
+        raise ProtocolError("Badly formatted reply line: unknown type %r"%tp)
+      else:
+        more = []
+        while 1:
+          line = self._s.readline()
+          if self._debugFile:
+            self._debugFile.write("+++ %s" % line)
+          if line in (".\r\n", ".\n", "650 OK\n", "650 OK\r\n"): 
+            break
+          more.append(line)
+        lines.append((code, s, unescape_dots("".join(more))))
+        isEvent = (lines and lines[0][0][0] == '6')
+        if isEvent: # Need "250 OK" if it's not an event. Otherwise, end
+          return (isEvent, lines)
+
+    # Notreached
+    raise TorCtlError()
+
+  def _doSend(self, msg):
+    if self._debugFile:
+      amsg = msg
+      lines = amsg.split("\n")
+      if len(lines) > 2:
+        amsg = "\n".join(lines[:2]) + "\n"
+      self._debugFile.write(str(time.time())+"\t>>> "+amsg)
+    self._s.write(msg)
+
+  def set_timer(self, in_seconds, type=None):
+    event = (("650", "TORCTL_TIMER", type),)
+    threading.Timer(in_seconds, lambda: 
+                  self._eventQueue.put((time.time(), event))).start()
+
+  def set_periodic_timer(self, every_seconds, type=None):
+    event = (("650", "TORCTL_TIMER", type),)
+    def notlambda():
+      plog("DEBUG", "Timer fired for type "+str(type))
+      self._eventQueue.put((time.time(), event))
+      self._eventQueue.put((time.time(), event))
+      threading.Timer(every_seconds, notlambda).start()
+    threading.Timer(every_seconds, notlambda).start()
+
+  def sendAndRecv(self, msg="", expectedTypes=("250", "251")):
+    """Helper: Send a command 'msg' to Tor, and wait for a command
+       in response.  If the response type is in expectedTypes,
+       return a list of (tp,body,extra) tuples.  If it is an
+       error, raise ErrorReply.  Otherwise, raise ProtocolError.
+    """
+    if type(msg) == types.ListType:
+      msg = "".join(msg)
+    assert msg.endswith("\r\n")
+
+    lines = self._sendImpl(self._doSend, msg)
+
+    # print lines
+    for tp, msg, _ in lines:
+      if tp[0] in '45':
+        code = int(tp[:3])
+        raise ErrorReply("%s %s"%(tp, msg), status = code, message = msg)
+      if tp not in expectedTypes:
+        raise ProtocolError("Unexpectd message type %r"%tp)
+
+    return lines
+
+  def authenticate(self, secret=""):
+    """Sends an authenticating secret (password) to Tor.  You'll need to call 
+       this method (or authenticate_cookie) before Tor can start.
+    """
+    #hexstr = binascii.b2a_hex(secret)
+    self.sendAndRecv("AUTHENTICATE \"%s\"\r\n"%secret)
+  
+  def authenticate_cookie(self, cookie):
+    """Sends an authentication cookie to Tor. This may either be a file or 
+       its contents.
+    """
+    
+    # read contents if provided a file
+    if type(cookie) == file: cookie = cookie.read()
+    
+    # unlike passwords the cookie contents isn't enclosed by quotes
+    self.sendAndRecv("AUTHENTICATE %s\r\n" % binascii.b2a_hex(cookie))
+
+  def get_option(self, name):
+    """Get the value of the configuration option named 'name'.  To
+       retrieve multiple values, pass a list for 'name' instead of
+       a string.  Returns a list of (key,value) pairs.
+       Refer to section 3.3 of control-spec.txt for a list of valid names.
+    """
+    if not isinstance(name, str):
+      name = " ".join(name)
+    lines = self.sendAndRecv("GETCONF %s\r\n" % name)
+
+    r = []
+    for _,line,_ in lines:
+      try:
+        key, val = line.split("=", 1)
+        r.append((key,val))
+      except ValueError:
+        r.append((line, None))
+
+    return r
+
+  def set_option(self, key, value):
+    """Set the value of the configuration option 'key' to the value 'value'.
+    """
+    self.set_options([(key, value)])
+
+  def set_options(self, kvlist):
+    """Given a list of (key,value) pairs, set them as configuration
+       options.
+    """
+    if not kvlist:
+      return
+    msg = " ".join(["%s=\"%s\""%(k,quote(v)) for k,v in kvlist])
+    self.sendAndRecv("SETCONF %s\r\n"%msg)
+
+  def reset_options(self, keylist):
+    """Reset the options listed in 'keylist' to their default values.
+
+       Tor started implementing this command in version 0.1.1.7-alpha;
+       previous versions wanted you to set configuration keys to "".
+       That no longer works.
+    """
+    self.sendAndRecv("RESETCONF %s\r\n"%(" ".join(keylist)))
+
+  def get_network_status(self, who="all"):
+    """Get the entire network status list. Returns a list of
+       TorCtl.NetworkStatus instances."""
+    return parse_ns_body(self.sendAndRecv("GETINFO ns/"+who+"\r\n")[0][2])
+
+  def get_address_mappings(self, type="all"):
+    # TODO: Also parse errors and GMTExpiry
+    body = self.sendAndRecv("GETINFO address-mappings/"+type+"\r\n")
+      
+    #print "|"+body[0][1].replace("address-mappings/"+type+"=", "")+"|"
+    #print str(body[0])
+
+    if body[0][1].replace("address-mappings/"+type+"=", "") != "":
+      # one line
+      lines = [body[0][1].replace("address-mappings/"+type+"=", "")]
+    elif not body[0][2]:
+      return []
+    else:
+      lines = body[0][2].split("\n")
+    if not lines: return []
+    ret = []
+    for l in lines:
+      #print "|"+str(l)+"|"
+      if len(l) == 0: continue #Skip last line.. it's empty
+      m = re.match(r'(\S+)\s+(\S+)\s+(\"[^"]+\"|\w+)', l)
+      if not m:
+        raise ProtocolError("ADDRMAP response misformatted.")
+      fromaddr, toaddr, when = m.groups()
+      if when.upper() == "NEVER":  
+        when = None
+      else:
+        when = time.strptime(when[1:-1], "%Y-%m-%d %H:%M:%S")
+      ret.append(AddrMap(fromaddr, toaddr, when))
+    return ret
+
+  def get_router(self, ns):
+    """Fill in a Router class corresponding to a given NS class"""
+    desc = self.sendAndRecv("GETINFO desc/id/" + ns.idhex + "\r\n")[0][2]
+    sig_start = desc.find("\nrouter-signature\n")+len("\nrouter-signature\n")
+    fp_base64 = sha1(desc[:sig_start]).digest().encode("base64")[:-2]
+    r = Router.build_from_desc(desc.split("\n"), ns)
+    if fp_base64 != ns.orhash:
+      plog("INFO", "Router descriptor for "+ns.idhex+" does not match ns fingerprint (NS @ "+str(ns.updated)+" vs Desc @ "+str(r.published)+")")
+      return None
+    else:
+      return r
+
+
+  def read_routers(self, nslist):
+    """ Given a list a NetworkStatuses in 'nslist', this function will 
+        return a list of new Router instances.
+    """
+    bad_key = 0
+    new = []
+    for ns in nslist:
+      try:
+        r = self.get_router(ns)
+        if r:
+          new.append(r)
+      except ErrorReply:
+        bad_key += 1
+        if "Running" in ns.flags:
+          plog("NOTICE", "Running router "+ns.nickname+"="
+             +ns.idhex+" has no descriptor")
+  
+    return new
+
+  def get_info(self, name):
+    """Return the value of the internal information field named 'name'.
+       Refer to section 3.9 of control-spec.txt for a list of valid names.
+       DOCDOC
+    """
+    if not isinstance(name, str):
+      name = " ".join(name)
+    lines = self.sendAndRecv("GETINFO %s\r\n"%name)
+    d = {}
+    for _,msg,more in lines:
+      if msg == "OK":
+        break
+      try:
+        k,rest = msg.split("=",1)
+      except ValueError:
+        raise ProtocolError("Bad info line %r",msg)
+      if more:
+        d[k] = more
+      else:
+        d[k] = rest
+    return d
+
+  def set_events(self, events, extended=False):
+    """Change the list of events that the event handler is interested
+       in to those in 'events', which is a list of event names.
+       Recognized event names are listed in section 3.3 of the control-spec
+    """
+    if extended:
+      plog ("DEBUG", "SETEVENTS EXTENDED %s\r\n" % " ".join(events))
+      self.sendAndRecv("SETEVENTS EXTENDED %s\r\n" % " ".join(events))
+    else:
+      self.sendAndRecv("SETEVENTS %s\r\n" % " ".join(events))
+
+  def save_conf(self):
+    """Flush all configuration changes to disk.
+    """
+    self.sendAndRecv("SAVECONF\r\n")
+
+  def send_signal(self, sig):
+    """Send the signal 'sig' to the Tor process; The allowed values for
+       'sig' are listed in section 3.6 of control-spec.
+    """
+    sig = { 0x01 : "HUP",
+        0x02 : "INT",
+        0x03 : "NEWNYM",
+        0x0A : "USR1",
+        0x0C : "USR2",
+        0x0F : "TERM" }.get(sig,sig)
+    self.sendAndRecv("SIGNAL %s\r\n"%sig)
+
+  def resolve(self, host):
+    """ Launch a remote hostname lookup request:
+        'host' may be a hostname or IPv4 address
+    """
+    # TODO: handle "mode=reverse"
+    self.sendAndRecv("RESOLVE %s\r\n"%host)
+
+  def map_address(self, kvList):
+    """ Sends the MAPADDRESS command for each of the tuples in kvList """
+    if not kvList:
+      return
+    m = " ".join([ "%s=%s" for k,v in kvList])
+    lines = self.sendAndRecv("MAPADDRESS %s\r\n"%m)
+    r = []
+    for _,line,_ in lines:
+      try:
+        key, val = line.split("=", 1)
+      except ValueError:
+        raise ProtocolError("Bad address line %r",v)
+      r.append((key,val))
+    return r
+
+  def extend_circuit(self, circid=None, hops=None):
+    """Tell Tor to extend the circuit identified by 'circid' through the
+       servers named in the list 'hops'.
+    """
+    if circid is None:
+      circid = 0
+    if hops is None:
+      hops = ""
+    plog("DEBUG", "Extending circuit")
+    lines = self.sendAndRecv("EXTENDCIRCUIT %d %s\r\n"
+                  %(circid, ",".join(hops)))
+    tp,msg,_ = lines[0]
+    m = re.match(r'EXTENDED (\S*)', msg)
+    if not m:
+      raise ProtocolError("Bad extended line %r",msg)
+    plog("DEBUG", "Circuit extended")
+    return int(m.group(1))
+
+  def redirect_stream(self, streamid, newaddr, newport=""):
+    """DOCDOC"""
+    if newport:
+      self.sendAndRecv("REDIRECTSTREAM %d %s %s\r\n"%(streamid, newaddr, newport))
+    else:
+      self.sendAndRecv("REDIRECTSTREAM %d %s\r\n"%(streamid, newaddr))
+
+  def attach_stream(self, streamid, circid, hop=None):
+    """Attach a stream to a circuit, specify both by IDs. If hop is given, 
+       try to use the specified hop in the circuit as the exit node for 
+       this stream.
+    """
+    if hop:
+      self.sendAndRecv("ATTACHSTREAM %d %d HOP=%d\r\n"%(streamid, circid, hop))
+      plog("DEBUG", "Attaching stream: "+str(streamid)+" to hop "+str(hop)+" of circuit "+str(circid))
+    else:
+      self.sendAndRecv("ATTACHSTREAM %d %d\r\n"%(streamid, circid))
+      plog("DEBUG", "Attaching stream: "+str(streamid)+" to circuit "+str(circid))
+
+  def close_stream(self, streamid, reason=0, flags=()):
+    """DOCDOC"""
+    self.sendAndRecv("CLOSESTREAM %d %s %s\r\n"
+              %(streamid, reason, "".join(flags)))
+
+  def close_circuit(self, circid, reason=0, flags=()):
+    """DOCDOC"""
+    self.sendAndRecv("CLOSECIRCUIT %d %s %s\r\n"
+              %(circid, reason, "".join(flags)))
+
+  def post_descriptor(self, desc):
+    self.sendAndRecv("+POSTDESCRIPTOR purpose=controller\r\n%s"%escape_dots(desc))
+
+def parse_ns_body(data):
+  """Parse the body of an NS event or command into a list of
+     NetworkStatus instances"""
+  if not data: return []
+  nsgroups = re.compile(r"^r ", re.M).split(data)
+  nsgroups.pop(0)
+  nslist = []
+  for nsline in nsgroups:
+    m = re.search(r"^s((?:[ ]\S*)+)", nsline, re.M)
+    flags = m.groups()
+    flags = flags[0].strip().split(" ")
+    m = re.match(r"(\S+)\s(\S+)\s(\S+)\s(\S+\s\S+)\s(\S+)\s(\d+)\s(\d+)", nsline)    
+    w = re.search(r"^w Bandwidth=(\d+)", nsline, re.M)
+    if w:
+      nslist.append(NetworkStatus(*(m.groups()+(flags,)+(int(w.group(1))*1000,))))
+    else:
+      nslist.append(NetworkStatus(*(m.groups() + (flags,))))
+  return nslist
+
+class EventSink:
+  def heartbeat_event(self, event): pass
+  def unknown_event(self, event): pass
+  def circ_status_event(self, event): pass
+  def stream_status_event(self, event): pass
+  def stream_bw_event(self, event): pass
+  def or_conn_status_event(self, event): pass
+  def bandwidth_event(self, event): pass
+  def new_desc_event(self, event): pass
+  def msg_event(self, event): pass
+  def ns_event(self, event): pass
+  def new_consensus_event(self, event): pass
+  def buildtimeout_set_event(self, event): pass
+  def guard_event(self, event): pass
+  def address_mapped_event(self, event): pass
+  def timer_event(self, event): pass
+
+class EventListener(EventSink):
+  """An 'EventListener' is a passive sink for parsed Tor events. It 
+     implements the same interface as EventHandler, but it should
+     not alter Tor's behavior as a result of these events.
+    
+     Do not extend from this class. Instead, extend from one of 
+     Pre, Post, or Dual event listener, to get events 
+     before, after, or before and after the EventHandler handles them.
+     """
+  def __init__(self):
+    """Create a new EventHandler."""
+    self._map1 = {
+      "CIRC" : self.circ_status_event,
+      "STREAM" : self.stream_status_event,
+      "ORCONN" : self.or_conn_status_event,
+      "STREAM_BW" : self.stream_bw_event,
+      "BW" : self.bandwidth_event,
+      "DEBUG" : self.msg_event,
+      "INFO" : self.msg_event,
+      "NOTICE" : self.msg_event,
+      "WARN" : self.msg_event,
+      "ERR" : self.msg_event,
+      "NEWDESC" : self.new_desc_event,
+      "ADDRMAP" : self.address_mapped_event,
+      "NS" : self.ns_event,
+      "NEWCONSENSUS" : self.new_consensus_event,
+      "BUILDTIMEOUT_SET" : self.buildtimeout_set_event,
+      "GUARD" : self.guard_event,
+      "TORCTL_TIMER" : self.timer_event
+      }
+    self.parent_handler = None
+    self._sabotage()
+
+  def _sabotage(self):
+    raise TorCtlError("Error: Do not extend from EventListener directly! Use Pre, Post or DualEventListener instead.")
+ 
+  def listen(self, event):
+    self.heartbeat_event(event)
+    self._map1.get(event.event_name, self.unknown_event)(event)
+
+  def set_parent(self, parent_handler):
+    self.parent_handler = parent_handler
+
+class PreEventListener(EventListener):
+  def _sabotage(self): pass
+class PostEventListener(EventListener):
+  def _sabotage(self): pass
+class DualEventListener(PreEventListener,PostEventListener): 
+  def _sabotage(self): pass
+
+class EventHandler(EventSink):
+  """An 'EventHandler' wraps callbacks for the events Tor can return. 
+     Each event argument is an instance of the corresponding event
+     class."""
+  def __init__(self):
+    """Create a new EventHandler."""
+    self._map1 = {
+      "CIRC" : self.circ_status_event,
+      "STREAM" : self.stream_status_event,
+      "ORCONN" : self.or_conn_status_event,
+      "STREAM_BW" : self.stream_bw_event,
+      "BW" : self.bandwidth_event,
+      "DEBUG" : self.msg_event,
+      "INFO" : self.msg_event,
+      "NOTICE" : self.msg_event,
+      "WARN" : self.msg_event,
+      "ERR" : self.msg_event,
+      "NEWDESC" : self.new_desc_event,
+      "ADDRMAP" : self.address_mapped_event,
+      "NS" : self.ns_event,
+      "NEWCONSENSUS" : self.new_consensus_event,
+      "BUILDTIMEOUT_SET" : self.buildtimeout_set_event,
+      "GUARD" : self.guard_event,
+      "TORCTL_TIMER" : self.timer_event
+      }
+    self.c = None # Gets set by Connection.set_event_hanlder()
+    self.pre_listeners = []
+    self.post_listeners = []
+
+  def _handle1(self, timestamp, lines):
+    """Dispatcher: called from Connection when an event is received."""
+    for code, msg, data in lines:
+      event = self._decode1(msg, data)
+      event.arrived_at = timestamp
+      event.state=EVENT_STATE.PRELISTEN
+      for l in self.pre_listeners:
+        l.listen(event)
+      event.state=EVENT_STATE.HEARTBEAT
+      self.heartbeat_event(event)
+      event.state=EVENT_STATE.HANDLING
+      self._map1.get(event.event_name, self.unknown_event)(event)
+      event.state=EVENT_STATE.POSTLISTEN
+      for l in self.post_listeners:
+        l.listen(event)
+
+  def _decode1(self, body, data):
+    """Unpack an event message into a type/arguments-tuple tuple."""
+    if " " in body:
+      evtype,body = body.split(" ",1)
+    else:
+      evtype,body = body,""
+    evtype = evtype.upper()
+    if evtype == "CIRC":
+      m = re.match(r"(\d+)\s+(\S+)(\s\S+)?(\s\S+)?(\s\S+)?(\s\S+)?", body)
+      if not m:
+        raise ProtocolError("CIRC event misformatted.")
+      ident,status,path,purpose,reason,remote = m.groups()
+      ident = int(ident)
+      if path:
+        if "PURPOSE=" in path:
+          remote = reason
+          reason = purpose
+          purpose=path
+          path=[]
+        elif "REASON=" in path:
+          remote = reason
+          reason = path
+          purpose = ""
+          path=[]
+        else:
+          path_verb = path.strip().split(",")
+          path = []
+          for p in path_verb:
+            path.append(p.replace("~", "=").split("=")[0])
+      else:
+        path = []
+
+      if purpose and "REASON=" in purpose:
+        remote=reason
+        reason=purpose
+        purpose=""
+
+      if purpose: purpose = purpose[9:]
+      if reason: reason = reason[8:]
+      if remote: remote = remote[15:]
+      event = CircuitEvent(evtype, ident, status, path, purpose, reason, remote)
+    elif evtype == "STREAM":
+      #plog("DEBUG", "STREAM: "+body)
+      m = re.match(r"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?:(\d+)(\sREASON=\S+)?(\sREMOTE_REASON=\S+)?(\sSOURCE=\S+)?(\sSOURCE_ADDR=\S+)?(\s+PURPOSE=\S+)?", body)
+      if not m:
+        raise ProtocolError("STREAM event misformatted.")
+      ident,status,circ,target_host,target_port,reason,remote,source,source_addr,purpose = m.groups()
+      ident,circ = map(int, (ident,circ))
+      if not target_host: # This can happen on SOCKS_PROTOCOL failures
+        target_host = "(none)"
+      if reason: reason = reason[8:]
+      if remote: remote = remote[15:]
+      if source: source = source[8:]
+      if source_addr: source_addr = source_addr[13:]
+      if purpose:
+        purpose = purpose.lstrip()
+        purpose = purpose[8:]
+      event = StreamEvent(evtype, ident, status, circ, target_host,
+               int(target_port), reason, remote, source, source_addr, purpose)
+    elif evtype == "ORCONN":
+      m = re.match(r"(\S+)\s+(\S+)(\sAGE=\S+)?(\sREAD=\S+)?(\sWRITTEN=\S+)?(\sREASON=\S+)?(\sNCIRCS=\S+)?", body)
+      if not m:
+        raise ProtocolError("ORCONN event misformatted.")
+      target, status, age, read, wrote, reason, ncircs = m.groups()
+
+      #plog("DEBUG", "ORCONN: "+body)
+      if ncircs: ncircs = int(ncircs[8:])
+      else: ncircs = 0
+      if reason: reason = reason[8:]
+      if age: age = int(age[5:])
+      else: age = 0
+      if read: read = int(read[6:])
+      else: read = 0
+      if wrote: wrote = int(wrote[9:])
+      else: wrote = 0
+      event = ORConnEvent(evtype, status, target, age, read, wrote,
+                reason, ncircs)
+    elif evtype == "STREAM_BW":
+      m = re.match(r"(\d+)\s+(\d+)\s+(\d+)", body)
+      if not m:
+        raise ProtocolError("STREAM_BW event misformatted.")
+      event = StreamBwEvent(evtype, *m.groups())
+    elif evtype == "BW":
+      m = re.match(r"(\d+)\s+(\d+)", body)
+      if not m:
+        raise ProtocolError("BANDWIDTH event misformatted.")
+      read, written = map(long, m.groups())
+      event = BWEvent(evtype, read, written)
+    elif evtype in ("DEBUG", "INFO", "NOTICE", "WARN", "ERR"):
+      event = LogEvent(evtype, body)
+    elif evtype == "NEWDESC":
+      ids_verb = body.split(" ")
+      ids = []
+      for i in ids_verb:
+        ids.append(i.replace("~", "=").split("=")[0].replace("$",""))
+      event = NewDescEvent(evtype, ids)
+    elif evtype == "ADDRMAP":
+      # TODO: Also parse errors and GMTExpiry
+      m = re.match(r'(\S+)\s+(\S+)\s+(\"[^"]+\"|\w+)', body)
+      if not m:
+        raise ProtocolError("ADDRMAP event misformatted.")
+      fromaddr, toaddr, when = m.groups()
+      if when.upper() == "NEVER":  
+        when = None
+      else:
+        when = time.strptime(when[1:-1], "%Y-%m-%d %H:%M:%S")
+      event = AddrMapEvent(evtype, fromaddr, toaddr, when)
+    elif evtype == "NS":
+      event = NetworkStatusEvent(evtype, parse_ns_body(data))
+    elif evtype == "NEWCONSENSUS":
+      event = NewConsensusEvent(evtype, parse_ns_body(data))
+    elif evtype == "BUILDTIMEOUT_SET":
+      m = re.match(
+        r"(\S+)\sTOTAL_TIMES=(\d+)\sTIMEOUT_MS=(\d+)\sXM=(\d+)\sALPHA=(\S+)\sCUTOFF_QUANTILE=(\S+)",
+        body)
+      set_type, total_times, timeout_ms, xm, alpha, quantile = m.groups()
+      event = BuildTimeoutSetEvent(evtype, set_type, int(total_times),
+                                   int(timeout_ms), int(xm), float(alpha),
+                                   float(quantile))
+    elif evtype == "GUARD":
+      m = re.match(r"(\S+)\s(\S+)\s(\S+)", body)
+      entry, guard, status = m.groups()
+      event = GuardEvent(evtype, entry, guard, status)
+    elif evtype == "TORCTL_TIMER":
+      event = TimerEvent(evtype, data)
+    else:
+      event = UnknownEvent(evtype, body)
+
+    return event
+
+  def add_event_listener(self, evlistener):
+    if isinstance(evlistener, PreEventListener):
+      self.pre_listeners.append(evlistener)
+    if isinstance(evlistener, PostEventListener):
+      self.post_listeners.append(evlistener)
+    evlistener.set_parent(self)
+
+  def heartbeat_event(self, event):
+    """Called before any event is received. Convenience function
+       for any cleanup/setup/reconfiguration you may need to do.
+    """
+    pass
+
+  def unknown_event(self, event):
+    """Called when we get an event type we don't recognize.  This
+       is almost alwyas an error.
+    """
+    pass
+
+  def circ_status_event(self, event):
+    """Called when a circuit status changes if listening to CIRCSTATUS
+       events."""
+    pass
+
+  def stream_status_event(self, event):
+    """Called when a stream status changes if listening to STREAMSTATUS
+       events.  """
+    pass
+
+  def stream_bw_event(self, event):
+    pass
+
+  def or_conn_status_event(self, event):
+    """Called when an OR connection's status changes if listening to
+       ORCONNSTATUS events."""
+    pass
+
+  def bandwidth_event(self, event):
+    """Called once a second if listening to BANDWIDTH events.
+    """
+    pass
+
+  def new_desc_event(self, event):
+    """Called when Tor learns a new server descriptor if listenting to
+       NEWDESC events.
+    """
+    pass
+
+  def msg_event(self, event):
+    """Called when a log message of a given severity arrives if listening
+       to INFO_MSG, NOTICE_MSG, WARN_MSG, or ERR_MSG events."""
+    pass
+
+  def ns_event(self, event):
+    pass
+
+  def new_consensus_event(self, event):
+    pass
+
+  def buildtimeout_set_event(self, event):
+    pass
+
+  def guard_event(self, event):
+    pass
+
+  def address_mapped_event(self, event):
+    """Called when Tor adds a mapping for an address if listening
+       to ADDRESSMAPPED events.
+    """
+    pass
+
+  def timer_event(self, event):
+    pass
+
+class Consensus:
+  """
+  A Consensus is a pickleable container for the members of
+  ConsensusTracker. This should only be used as a temporary 
+  reference, and will change after a NEWDESC or NEWCONSENUS event.
+  If you want a copy of a consensus that is independent
+  of subsequent updates, use copy.deepcopy()
+  """
+
+  def __init__(self, ns_map, sorted_r, router_map, nick_map, consensus_count):
+    self.ns_map = ns_map
+    self.sorted_r = sorted_r
+    self.routers = router_map
+    self.name_to_key = nick_map
+    self.consensus_count = consensus_count
+
+class ConsensusTracker(EventHandler):
+  """
+  A ConsensusTracker is an EventHandler that tracks the current
+  consensus of Tor in self.ns_map, self.routers and self.sorted_r
+  """
+  def __init__(self, c, RouterClass=Router):
+    EventHandler.__init__(self)
+    c.set_event_handler(self)
+    self.ns_map = {}
+    self.routers = {}
+    self.sorted_r = []
+    self.name_to_key = {}
+    self.RouterClass = RouterClass
+    self.consensus_count = 0
+    self.update_consensus()
+
+  # XXX: If there were a potential memory leak through perpetually referenced
+  # objects, this function would be the #1 suspect.
+  def _read_routers(self, nslist):
+    # Routers can fall out of our consensus five different ways:
+    # 1. Their descriptors disappear
+    # 2. Their NS documents disappear
+    # 3. They lose the Running flag
+    # 4. They list a bandwidth of 0
+    # 5. They have 'opt hibernating' set
+    routers = self.c.read_routers(nslist) # Sets .down if 3,4,5
+    self.consensus_count = len(routers)
+    old_idhexes = set(self.routers.keys())
+    new_idhexes = set(map(lambda r: r.idhex, routers)) 
+    for r in routers:
+      if r.idhex in self.routers:
+        if self.routers[r.idhex].nickname != r.nickname:
+          plog("NOTICE", "Router "+r.idhex+" changed names from "
+             +self.routers[r.idhex].nickname+" to "+r.nickname)
+        # Must do IN-PLACE update to keep all the refs to this router
+        # valid and current (especially for stats)
+        self.routers[r.idhex].update_to(r)
+      else:
+        rc = self.RouterClass(r)
+        self.routers[rc.idhex] = rc
+
+    removed_idhexes = old_idhexes - new_idhexes
+    removed_idhexes.update(set(map(lambda r: r.idhex,
+                                   filter(lambda r: r.down, routers))))
+
+    for i in removed_idhexes:
+      if i not in self.routers: continue
+      self.routers[i].down = True
+      if "Running" in self.routers[i].flags:
+        self.routers[i].flags.remove("Running")
+      if self.routers[i].refcount == 0:
+        self.routers[i].deleted = True
+        if self.routers[i].__class__.__name__ == "StatsRouter":
+          plog("WARN", "Expiring non-running StatsRouter "+i)
+        else:
+          plog("INFO", "Expiring non-running router "+i)
+        del self.routers[i]
+      else:
+        plog("INFO", "Postponing expiring non-running router "+i)
+        self.routers[i].deleted = True
+
+    self.sorted_r = filter(lambda r: not r.down, self.routers.itervalues())
+    self.sorted_r.sort(lambda x, y: cmp(y.bw, x.bw))
+    for i in xrange(len(self.sorted_r)): self.sorted_r[i].list_rank = i
+
+    # XXX: Verification only. Can be removed.
+    self._sanity_check(self.sorted_r)
+
+  def _sanity_check(self, list):
+    if len(self.routers) > 1.5*self.consensus_count:
+      plog("WARN", "Router count of "+str(len(self.routers))+" exceeds consensus count "+str(self.consensus_count)+" by more than 50%")
+
+    if len(self.ns_map) < self.consensus_count:
+      plog("WARN", "NS map count of "+str(len(self.ns_map))+" is below consensus count "+str(self.consensus_count))
+
+    downed =  filter(lambda r: r.down, list)
+    for d in downed:
+      plog("WARN", "Router "+d.idhex+" still present but is down. Del: "+str(d.deleted)+", flags: "+str(d.flags)+", bw: "+str(d.bw))
+ 
+    deleted =  filter(lambda r: r.deleted, list)
+    for d in deleted:
+      plog("WARN", "Router "+d.idhex+" still present but is deleted. Down: "+str(d.down)+", flags: "+str(d.flags)+", bw: "+str(d.bw))
+
+    zero =  filter(lambda r: r.refcount == 0 and r.__class__.__name__ == "StatsRouter", list)
+    for d in zero:
+      plog("WARN", "Router "+d.idhex+" has refcount 0. Del:"+str(d.deleted)+", Down: "+str(d.down)+", flags: "+str(d.flags)+", bw: "+str(d.bw))
+ 
+  def _update_consensus(self, nslist):
+    self.ns_map = {}
+    for n in nslist:
+      self.ns_map[n.idhex] = n
+      self.name_to_key[n.nickname] = "$"+n.idhex
+   
+  def update_consensus(self):
+    self._update_consensus(self.c.get_network_status())
+    self._read_routers(self.ns_map.values())
+
+  def new_consensus_event(self, n):
+    self._update_consensus(n.nslist)
+    self._read_routers(self.ns_map.values())
+    plog("DEBUG", str(time.time()-n.arrived_at)+" Read " + str(len(n.nslist))
+       +" NC => " + str(len(self.sorted_r)) + " routers")
+ 
+  def new_desc_event(self, d):
+    update = False
+    for i in d.idlist:
+      r = None
+      try:
+        ns = self.c.get_network_status("id/"+i)
+        r = self.c.read_routers(ns)
+      except ErrorReply, e:
+        plog("WARN", "Error reply for "+i+" after NEWDESC: "+str(e))
+        continue
+      if not r:
+        plog("WARN", "No router desc for "+i+" after NEWDESC")
+        continue
+      elif len(r) != 1:
+        plog("WARN", "Multiple descs for "+i+" after NEWDESC")
+
+      r = r[0]
+      ns = ns[0]
+      if ns.idhex in self.routers and self.routers[ns.idhex].orhash == r.orhash:
+        plog("NOTICE",
+             "Got extra NEWDESC event for router "+ns.nickname+"="+ns.idhex)
+      else:
+        self.consensus_count += 1
+      self.name_to_key[ns.nickname] = "$"+ns.idhex
+      if r and r.idhex in self.ns_map:
+        if ns.orhash != self.ns_map[r.idhex].orhash:
+          plog("WARN", "Getinfo and consensus disagree for "+r.idhex)
+          continue
+        update = True
+        if r.idhex in self.routers:
+          self.routers[r.idhex].update_to(r)
+        else:
+          self.routers[r.idhex] = self.RouterClass(r)
+    if update:
+      self.sorted_r = filter(lambda r: not r.down, self.routers.itervalues())
+      self.sorted_r.sort(lambda x, y: cmp(y.bw, x.bw))
+      for i in xrange(len(self.sorted_r)): self.sorted_r[i].list_rank = i
+    plog("DEBUG", str(time.time()-d.arrived_at)+ " Read " + str(len(d.idlist))
+       +" ND => "+str(len(self.sorted_r))+" routers. Update: "+str(update))
+    # XXX: Verification only. Can be removed.
+    self._sanity_check(self.sorted_r)
+    return update
+
+  def current_consensus(self):
+    return Consensus(self.ns_map, self.sorted_r, self.routers, 
+                     self.name_to_key, self.consensus_count)
+
+class DebugEventHandler(EventHandler):
+  """Trivial debug event handler: reassembles all parsed events to stdout."""
+  def circ_status_event(self, circ_event): # CircuitEvent()
+    output = [circ_event.event_name, str(circ_event.circ_id),
+          circ_event.status]
+    if circ_event.path:
+      output.append(",".join(circ_event.path))
+    if circ_event.reason:
+      output.append("REASON=" + circ_event.reason)
+    if circ_event.remote_reason:
+      output.append("REMOTE_REASON=" + circ_event.remote_reason)
+    print " ".join(output)
+
+  def stream_status_event(self, strm_event):
+    output = [strm_event.event_name, str(strm_event.strm_id),
+          strm_event.status, str(strm_event.circ_id),
+          strm_event.target_host, str(strm_event.target_port)]
+    if strm_event.reason:
+      output.append("REASON=" + strm_event.reason)
+    if strm_event.remote_reason:
+      output.append("REMOTE_REASON=" + strm_event.remote_reason)
+    print " ".join(output)
+
+  def ns_event(self, ns_event):
+    for ns in ns_event.nslist:
+      print " ".join((ns_event.event_name, ns.nickname, ns.idhash,
+        ns.updated.isoformat(), ns.ip, str(ns.orport),
+        str(ns.dirport), " ".join(ns.flags)))
+
+  def new_consensus_event(self, nc_event):
+    self.ns_event(nc_event)
+
+  def new_desc_event(self, newdesc_event):
+    print " ".join((newdesc_event.event_name, " ".join(newdesc_event.idlist)))
+   
+  def or_conn_status_event(self, orconn_event):
+    if orconn_event.age: age = "AGE="+str(orconn_event.age)
+    else: age = ""
+    if orconn_event.read_bytes: read = "READ="+str(orconn_event.read_bytes)
+    else: read = ""
+    if orconn_event.wrote_bytes: wrote = "WRITTEN="+str(orconn_event.wrote_bytes)
+    else: wrote = ""
+    if orconn_event.reason: reason = "REASON="+orconn_event.reason
+    else: reason = ""
+    if orconn_event.ncircs: ncircs = "NCIRCS="+str(orconn_event.ncircs)
+    else: ncircs = ""
+    print " ".join((orconn_event.event_name, orconn_event.endpoint,
+            orconn_event.status, age, read, wrote, reason, ncircs))
+
+  def msg_event(self, log_event):
+    print log_event.event_name+" "+log_event.msg
+  
+  def bandwidth_event(self, bw_event):
+    print bw_event.event_name+" "+str(bw_event.read)+" "+str(bw_event.written)
+
+def parseHostAndPort(h):
+  """Given a string of the form 'address:port' or 'address' or
+     'port' or '', return a two-tuple of (address, port)
+  """
+  host, port = "localhost", 9100
+  if ":" in h:
+    i = h.index(":")
+    host = h[:i]
+    try:
+      port = int(h[i+1:])
+    except ValueError:
+      print "Bad hostname %r"%h
+      sys.exit(1)
+  elif h:
+    try:
+      port = int(h)
+    except ValueError:
+      host = h
+
+  return host, port
+
+def run_example(host,port):
+  """ Example of basic TorCtl usage. See PathSupport for more advanced
+      usage.
+  """
+  print "host is %s:%d"%(host,port)
+  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+  s.connect((host,port))
+  c = Connection(s)
+  c.set_event_handler(DebugEventHandler())
+  th = c.launch_thread()
+  c.authenticate()
+  print "nick",`c.get_option("nickname")`
+  print `c.get_info("version")`
+  #print `c.get_info("desc/name/moria1")`
+  print `c.get_info("network-status")`
+  print `c.get_info("addr-mappings/all")`
+  print `c.get_info("addr-mappings/config")`
+  print `c.get_info("addr-mappings/cache")`
+  print `c.get_info("addr-mappings/control")`
+
+  print `c.extend_circuit(0,["moria1"])`
+  try:
+    print `c.extend_circuit(0,[""])`
+  except ErrorReply: # wtf?
+    print "got error. good."
+  except:
+    print "Strange error", sys.exc_info()[0]
+   
+  #send_signal(s,1)
+  #save_conf(s)
+
+  #set_option(s,"1")
+  #set_option(s,"bandwidthburstbytes 100000")
+  #set_option(s,"runasdaemon 1")
+  #set_events(s,[EVENT_TYPE.WARN])
+#  c.set_events([EVENT_TYPE.ORCONN], True)
+  c.set_events([EVENT_TYPE.STREAM, EVENT_TYPE.CIRC,
+          EVENT_TYPE.NEWCONSENSUS, EVENT_TYPE.NEWDESC,
+          EVENT_TYPE.ORCONN, EVENT_TYPE.BW], True)
+
+  th.join()
+  return
+
+if __name__ == '__main__':
+  if len(sys.argv) > 2:
+    print "Syntax: TorControl.py torhost:torport"
+    sys.exit(0)
+  else:
+    sys.argv.append("localhost:9051")
+  sh,sp = parseHostAndPort(sys.argv[1])
+  run_example(sh,sp)
+


Property changes on: arm/dependencies/TorCtl/TorCtl.py
___________________________________________________________________
Added: svn:executable
   + *

Added: arm/dependencies/TorCtl/TorUtil.py
===================================================================
--- arm/dependencies/TorCtl/TorUtil.py	                        (rev 0)
+++ arm/dependencies/TorCtl/TorUtil.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,412 @@
+#!/usr/bin/python
+# TorCtl.py -- Python module to interface with Tor Control interface.
+# Copyright 2007 Mike Perry -- See LICENSE for licensing information.
+# Portions Copyright 2005 Nick Matthewson
+
+"""
+TorUtil -- Support functions for TorCtl.py and metatroller
+"""
+
+import os
+import re
+import sys
+import socket
+import binascii
+import math
+import time
+import logging
+import ConfigParser
+
+if sys.version_info < (2, 5):
+  from sha import sha as sha1
+else:
+  from hashlib import sha1
+
+__all__ = ["Enum", "Enum2", "Callable", "sort_list", "quote", "escape_dots", "unescape_dots",
+      "BufSock", "secret_to_key", "urandom_rng", "s2k_gen", "s2k_check", "plog", 
+     "ListenSocket", "zprob", "logfile", "loglevel"]
+
+# TODO: This isn't the right place for these.. But at least it's unified.
+tor_port = 9060
+tor_host = '127.0.0.1'
+
+control_port = 9061
+control_host = '127.0.0.1'
+control_pass = ""
+
+meta_port = 9052
+meta_host = '127.0.0.1'
+
+class Referrer:
+  def __init__(self, cl):
+    self.referrers = {}
+    self.cl_name = cl
+    self.count = 0
+
+  def recurse_store(self, gc, obj, depth, max_depth):
+    if depth >= max_depth: return
+    for r in gc.get_referrers(obj):
+      if hasattr(r, "__class__"):
+        cl = r.__class__.__name__
+        # Skip frames and list iterators.. prob just us
+        if cl in ("frame", "listiterator"): continue 
+        if cl not in self.referrers:
+          self.referrers[cl] = Referrer(cl)
+        self.referrers[cl].count += 1
+        self.referrers[cl].recurse_store(gc, r, depth+1, max_depth)
+
+  def recurse_print(self, rcutoff, depth=""):
+    refs = self.referrers.keys()
+    refs.sort(lambda x, y: self.referrers[y].count - self.referrers[x].count)
+    for r in refs:
+      if self.referrers[r].count > rcutoff:
+        plog("NOTICE", "GC:  "+depth+"Refed by "+r+": "+str(self.referrers[r].count))
+        self.referrers[r].recurse_print(rcutoff, depth+" ")
+
+def dump_class_ref_counts(referrer_depth=2, cutoff=500, rcutoff=1,
+        ignore=('tuple', 'list', 'function', 'dict',
+                 'builtin_function_or_method',
+                 'wrapper_descriptor')):
+  """ Debugging function to track down types of objects
+      that cannot be garbage collected because we hold refs to them 
+      somewhere."""
+  import gc
+  __dump_class_ref_counts(gc, referrer_depth, cutoff, rcutoff, ignore)
+  gc.collect()
+  plog("NOTICE", "GC: Done.")
+
+def __dump_class_ref_counts(gc, referrer_depth, cutoff, rcutoff, ignore):
+  """ loil
+  """
+  plog("NOTICE", "GC: Gathering garbage collection stats...")
+  uncollectable = gc.collect()
+  class_counts = {}
+  referrers = {}
+  plog("NOTICE", "GC: Uncollectable objects: "+str(uncollectable))
+  objs = gc.get_objects()
+  for obj in objs:
+    if hasattr(obj, "__class__"):
+      cl = obj.__class__.__name__
+      if cl in ignore: continue
+      if cl not in class_counts:
+        class_counts[cl] = 0
+        referrers[cl] = Referrer(cl)
+      class_counts[cl] += 1
+  if referrer_depth:
+    for obj in objs:
+      if hasattr(obj, "__class__"):
+        cl = obj.__class__.__name__
+        if cl in ignore: continue
+        if class_counts[cl] > cutoff:
+          referrers[cl].recurse_store(gc, obj, 0, referrer_depth)
+  classes = class_counts.keys()
+  classes.sort(lambda x, y: class_counts[y] - class_counts[x])
+  for c in classes:
+    if class_counts[c] < cutoff: continue
+    plog("NOTICE", "GC: Class "+c+": "+str(class_counts[c]))
+    if referrer_depth:
+      referrers[c].recurse_print(rcutoff)
+
+
+
+def read_config(filename):
+  config = ConfigParser.SafeConfigParser()
+  config.read(filename)
+  global tor_port, tor_host, control_port, control_pass, control_host
+  global meta_port, meta_host
+  global loglevel
+
+  tor_port = config.getint('TorCtl', 'tor_port')
+  meta_port = config.getint('TorCtl', 'meta_port')
+  control_port = config.getint('TorCtl', 'control_port')
+
+  tor_host = config.get('TorCtl', 'tor_host')
+  control_host = config.get('TorCtl', 'control_host')
+  meta_host = config.get('TorCtl', 'meta_host')
+  control_pass = config.get('TorCtl', 'control_pass')
+  loglevel = config.get('TorCtl', 'loglevel')
+
+
+class Enum:
+  """ Defines an ordered dense name-to-number 1-1 mapping """
+  def __init__(self, start, names):
+    self.nameOf = {}
+    idx = start
+    for name in names:
+      setattr(self,name,idx)
+      self.nameOf[idx] = name
+      idx += 1
+
+class Enum2:
+  """ Defines an ordered sparse name-to-number 1-1 mapping """
+  def __init__(self, **args):
+    self.__dict__.update(args)
+    self.nameOf = {}
+    for k,v in args.items():
+      self.nameOf[v] = k
+
+class Callable:
+    def __init__(self, anycallable):
+        self.__call__ = anycallable
+
+def sort_list(list, key):
+  """ Sort a list by a specified key """
+  list.sort(lambda x,y: cmp(key(x), key(y))) # Python < 2.4 hack
+  return list
+
+def quote(s):
+  return re.sub(r'([\r\n\\\"])', r'\\\1', s)
+
+def escape_dots(s, translate_nl=1):
+  if translate_nl:
+    lines = re.split(r"\r?\n", s)
+  else:
+    lines = s.split("\r\n")
+  if lines and not lines[-1]:
+    del lines[-1]
+  for i in xrange(len(lines)):
+    if lines[i].startswith("."):
+      lines[i] = "."+lines[i]
+  lines.append(".\r\n")
+  return "\r\n".join(lines)
+
+def unescape_dots(s, translate_nl=1):
+  lines = s.split("\r\n")
+
+  for i in xrange(len(lines)):
+    if lines[i].startswith("."):
+      lines[i] = lines[i][1:]
+
+  if lines and lines[-1]:
+    lines.append("")
+
+  if translate_nl:
+    return "\n".join(lines)
+  else:
+    return "\r\n".join(lines)
+
+# XXX: Exception handling
+class BufSock:
+  def __init__(self, s):
+    self._s = s
+    self._buf = []
+
+  def readline(self):
+    if self._buf:
+      idx = self._buf[0].find('\n')
+      if idx >= 0:
+        result = self._buf[0][:idx+1]
+        self._buf[0] = self._buf[0][idx+1:]
+        return result
+
+    while 1:
+      s = self._s.recv(128)
+      if not s: return None
+      # XXX: This really does need an exception
+      #  raise ConnectionClosed()
+      idx = s.find('\n')
+      if idx >= 0:
+        self._buf.append(s[:idx+1])
+        result = "".join(self._buf)
+        rest = s[idx+1:]
+        if rest:
+          self._buf = [ rest ]
+        else:
+          del self._buf[:]
+        return result
+      else:
+        self._buf.append(s)
+
+  def write(self, s):
+    self._s.send(s)
+
+  def close(self):
+    self._s.close()
+
+# SocketServer.TCPServer is nuts.. 
+class ListenSocket:
+  def __init__(self, listen_ip, port):
+    msg = None
+    self.s = None
+    for res in socket.getaddrinfo(listen_ip, port, socket.AF_UNSPEC,
+              socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
+      af, socktype, proto, canonname, sa = res
+      try:
+        self.s = socket.socket(af, socktype, proto)
+        self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+      except socket.error, msg:
+        self.s = None
+        continue
+      try:
+        self.s.bind(sa)
+        self.s.listen(1)
+      except socket.error, msg:
+        self.s.close()
+        self.s = None
+        continue
+      break
+    if self.s is None:
+      raise socket.error(msg)
+
+  def accept(self):
+    conn, addr = self.s.accept()
+    return conn
+
+  def close(self):
+    self.s.close()
+
+
+def secret_to_key(secret, s2k_specifier):
+  """Used to generate a hashed password string. DOCDOC."""
+  c = ord(s2k_specifier[8])
+  EXPBIAS = 6
+  count = (16+(c&15)) << ((c>>4) + EXPBIAS)
+
+  d = sha1()
+  tmp = s2k_specifier[:8]+secret
+  slen = len(tmp)
+  while count:
+    if count > slen:
+      d.update(tmp)
+      count -= slen
+    else:
+      d.update(tmp[:count])
+      count = 0
+  return d.digest()
+
+def urandom_rng(n):
+  """Try to read some entropy from the platform entropy source."""
+  f = open('/dev/urandom', 'rb')
+  try:
+    return f.read(n)
+  finally:
+    f.close()
+
+def s2k_gen(secret, rng=None):
+  """DOCDOC"""
+  if rng is None:
+    if hasattr(os, "urandom"):
+      rng = os.urandom
+    else:
+      rng = urandom_rng
+  spec = "%s%s"%(rng(8), chr(96))
+  return "16:%s"%(
+    binascii.b2a_hex(spec + secret_to_key(secret, spec)))
+
+def s2k_check(secret, k):
+  """DOCDOC"""
+  assert k[:3] == "16:"
+
+  k =  binascii.a2b_hex(k[3:])
+  return secret_to_key(secret, k[:9]) == k[9:]
+
+## XXX: Make this a class?
+loglevel = "DEBUG"
+#loglevels = {"DEBUG" : 0, "INFO" : 1, "NOTICE" : 2, "WARN" : 3, "ERROR" : 4, "NONE" : 5}
+logfile = None
+logger = None
+
+# Python logging levels are in increments of 10, so place our custom
+# levels in between Python's default levels.
+loglevels = { "DEBUG":  logging.DEBUG,
+              "INFO":   logging.INFO,
+              "NOTICE": logging.INFO + 5,
+              "WARN":   logging.WARN,
+              "ERROR":  logging.ERROR,
+              "NONE":   logging.ERROR + 5 }
+# Set loglevel => name translation.
+for name, value in loglevels.iteritems():
+  logging.addLevelName(value, name)
+
+def plog_use_logger(name):
+  """ Set the Python logger to use with plog() by name.
+      Useful when TorCtl is integrated with an application using logging.
+      The logger specified by name must be set up before the first call
+      to plog()! """
+  global logger, loglevels
+  logger = logging.getLogger(name)
+
+def plog(level, msg, *args):
+  global logger, logfile
+  if not logger:
+    # Default init = old TorCtl format + default behavior
+    # Default behavior = log to stdout if TorUtil.logfile is None,
+    # or to the open file specified otherwise.
+    logger = logging.getLogger("TorCtl")
+    formatter = logging.Formatter("%(levelname)s[%(asctime)s]:%(message)s",
+                                  "%a %b %d %H:%M:%S %Y")
+
+    if not logfile:
+      logfile = sys.stdout
+    # HACK: if logfile is a string, assume is it the desired filename.
+    if type(logfile) is str:
+      f = logging.FileHandler(logfile)
+      f.setFormatter(formatter)
+      logger.addHandler(f)
+    # otherwise, pretend it is a stream.
+    else:
+      ch = logging.StreamHandler(logfile)
+      ch.setFormatter(formatter)
+      logger.addHandler(ch)
+    logger.setLevel(loglevels[loglevel])
+
+  logger.log(loglevels[level], msg, *args)
+
+# The following zprob routine was stolen from
+# http://www.nmr.mgh.harvard.edu/Neural_Systems_Group/gary/python/stats.py
+# pursuant to this license:
+#
+# Copyright (c) 1999-2007 Gary Strangman; All Rights Reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# The above license applies only to the following 39 lines of code.
+def zprob(z):
+    """
+Returns the area under the normal curve 'to the left of' the given z value.
+Thus, 
+    for z<0, zprob(z) = 1-tail probability
+    for z>0, 1.0-zprob(z) = 1-tail probability
+    for any z, 2.0*(1.0-zprob(abs(z))) = 2-tail probability
+Adapted from z.c in Gary Perlman's |Stat.
+
+Usage:   lzprob(z)
+"""
+    Z_MAX = 6.0    # maximum meaningful z-value
+    if z == 0.0:
+        x = 0.0
+    else:
+        y = 0.5 * math.fabs(z)
+        if y >= (Z_MAX*0.5):
+            x = 1.0
+        elif (y < 1.0):
+            w = y*y
+            x = ((((((((0.000124818987 * w
+                        -0.001075204047) * w +0.005198775019) * w
+                      -0.019198292004) * w +0.059054035642) * w
+                    -0.151968751364) * w +0.319152932694) * w
+                  -0.531923007300) * w +0.797884560593) * y * 2.0
+        else:
+            y = y - 2.0
+            x = (((((((((((((-0.000045255659 * y
+                             +0.000152529290) * y -0.000019538132) * y
+                           -0.000676904986) * y +0.001390604284) * y
+                         -0.000794620820) * y -0.002034254874) * y
+                       +0.006549791214) * y -0.010557625006) * y
+                     +0.011630447319) * y -0.009279453341) * y
+                   +0.005353579108) * y -0.002141268741) * y
+                 +0.000535310849) * y +0.999936657524
+    if z > 0.0:
+        prob = ((x+1.0)*0.5)
+    else:
+        prob = ((1.0-x)*0.5)
+    return prob
+

Added: arm/dependencies/TorCtl/__init__.py
===================================================================
--- arm/dependencies/TorCtl/__init__.py	                        (rev 0)
+++ arm/dependencies/TorCtl/__init__.py	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,28 @@
+"""
+TorCtl is a python Tor controller with extensions to support path
+building and various constraints on node and path selection, as well as
+statistics gathering.
+
+Apps can hook into the TorCtl package at whatever level they wish. 
+
+The lowest level of interaction is to use the TorCtl module
+(TorCtl/TorCtl.py). Typically this is done by importing TorCtl.TorCtl
+and creating a TorCtl.Connection and extending from TorCtl.EventHandler.
+This class receives Tor controller events packaged into python classes
+from a TorCtl.Connection.
+
+The next level up is to use the TorCtl.PathSupport module. This is done
+by importing TorCtl.PathSupport and instantiating or extending from
+PathSupport.PathBuilder, which itself extends from TorCtl.EventHandler.
+This class handles circuit construction and stream attachment subject to
+policies defined by PathSupport.NodeRestrictor and
+PathSupport.PathRestrictor implementations.
+
+If you are interested in gathering statistics, you can instead
+instantiate or extend from StatsSupport.StatsHandler, which is
+again an event handler with hooks to record statistics on circuit
+creation, stream bandwidth, and circuit failure information.
+"""
+
+__all__ = ["TorUtil", "GeoIPSupport", "PathSupport", "TorCtl", "StatsSupport",
+           "SQLSupport", "ScanSupport"]

Added: arm/dependencies/notes.txt
===================================================================
--- arm/dependencies/notes.txt	                        (rev 0)
+++ arm/dependencies/notes.txt	2010-08-23 01:13:01 UTC (rev 23018)
@@ -0,0 +1,6 @@
+TorCtl -
+  Last Updated: 8/22/10 (c514a0a7105cebe7cc5fa199750b90369b820bfb):
+  To update run the following:
+    git clone git://git.torproject.org/pytorctl.git
+    git archive master | tar -x -C /path/to/dependences/TorCtl/
+


Property changes on: arm/release
___________________________________________________________________
Modified: svn:externals
   - TorCtl https://svn.torproject.org/svn/torctl/trunk/python/TorCtl

   + TorCtl https://svn.torproject.org/svn/arm/dependencies/TorCtl



Property changes on: arm/trunk
___________________________________________________________________
Deleted: svn:externals
   - TorCtl https://svn.torproject.org/svn/torctl/trunk/python/TorCtl



Property changes on: arm/trunk/src
___________________________________________________________________
Added: svn:externals
   + TorCtl https://svn.torproject.org/svn/arm/dependencies/TorCtl




More information about the tor-commits mailing list