[tor-commits] [ooni-probe/master] * Added pre-chained start_tor_filter_nodes() Tor utility.

isis at torproject.org isis at torproject.org
Thu Oct 4 14:41:15 UTC 2012


commit f9f97ae03f85c4257ef26939d02f9dabc546ac8d
Author: Isis Lovecruft <isis at torproject.org>
Date:   Tue Oct 2 09:01:43 2012 +0000

    * Added pre-chained start_tor_filter_nodes() Tor utility.
    * Fixed the Deferred class decorator utility.
    * Added more Sphinx/pydoctor(markdownable) documentation.
    * Removed a couple unnecessary imports.
---
 ooni/plugins/bridget.py |  123 +++++++++-----
 ooni/utils/log.py       |    8 +-
 ooni/utils/onion.py     |  425 ++++++++++++++++++++++++++++++++++++++--------
 ooni/utils/timer.py     |   64 +++++++-
 4 files changed, 495 insertions(+), 125 deletions(-)

diff --git a/ooni/plugins/bridget.py b/ooni/plugins/bridget.py
index 538e2f3..92748c1 100644
--- a/ooni/plugins/bridget.py
+++ b/ooni/plugins/bridget.py
@@ -279,10 +279,11 @@ class BridgetTest(OONITest):
         """
         try:
             from ooni.utils         import process
-            from ooni.utils.onion   import start_tor, remove_public_relays
-            from ooni.utils.onion   import setup_done, setup_fail
+            from ooni.utils.onion   import remove_public_relays, start_tor
+            from ooni.utils.onion   import start_tor_filter_nodes
+            from ooni.utils.onion   import setup_fail, setup_done
             from ooni.utils.onion   import CustomCircuit
-            from ooni.utils.timer   import timeout
+            from ooni.utils.timer   import deferred_timeout, TimeoutError
             from ooni.lib.txtorcon  import TorConfig, TorState
         except ImportError:
             raise TxtorconImportError
@@ -291,12 +292,20 @@ class BridgetTest(OONITest):
             sys.exit()
 
         def reconfigure_done(state, bridges):
+            """
+            Append :ivar:`bridges['current']` to the list 
+            :ivar:`bridges['up'].
+            """
             log.msg("Reconfiguring with 'Bridge %s' successful" 
                     % bridges['current'])
             bridges['up'].append(bridges['current'])
             return state
 
         def reconfigure_fail(state, bridges):
+            """
+            Append :ivar:`bridges['current']` to the list 
+            :ivar:`bridges['down'].
+            """
             log.msg("Reconfiguring TorConfig with parameters %s failed"
                     % state)
             bridges['down'].append(bridges['current'])
@@ -307,8 +316,28 @@ class BridgetTest(OONITest):
             """
             Rewrite the Bridge line in our torrc. If use of pluggable
             transports was specified, rewrite the line as:
-                Bridge <transport_type> <ip>:<orport>
-            Otherwise, rewrite in the standard form.
+                Bridge <transport_type> <IP>:<ORPort>
+            Otherwise, rewrite in the standard form:
+                Bridge <IP>:<ORPort>
+
+            :param state:
+                A fully bootstrapped instance of 
+                :class:`ooni.lib.txtorcon.TorState`.
+            :param bridges:
+                A dictionary of bridges containing the following keys:
+
+                bridges['remaining'] :: A function returning and int for the
+                                        number of remaining bridges to test.
+                bridges['current']   :: A string containing the <IP>:<ORPort>
+                                        of the current bridge.
+                bridges['use_pt']    :: A boolean, True if we're testing
+                                        bridges with a pluggable transport;
+                                        False otherwise.
+                bridges['pt_type']   :: If :ivar:`bridges['use_pt'] is True,
+                                        this is a string containing the type
+                                        of pluggable transport to test.
+            :return:
+                :param:`state`
             """
             log.msg("Current Bridge: %s" % bridges['current'])
             log.msg("We now have %d bridges remaining to test..." 
@@ -324,11 +353,11 @@ class BridgetTest(OONITest):
                     raise PTNotFoundException
 
                 if controller_response == 'OK':
-                    finish = reconfigure_done(state, bridges)
+                    finish = yield reconfigure_done(state, bridges)
                 else:
                     log.err("SETCONF for %s responded with error:\n %s"
                             % (bridges['current'], controller_response))
-                    finish = reconfigure_fail(state, bridges)
+                    finish = yield reconfigure_fail(state, bridges)
 
                 defer.returnValue(finish)
 
@@ -365,51 +394,46 @@ class BridgetTest(OONITest):
         def state_attach_fail(state):
             log.err("Attaching custom circuit builder failed: %s" % state)
 
-
         log.msg("Bridget: initiating test ... ")  ## Start the experiment
 
+        ## if we've at least one bridge, and our config has no 'Bridge' line
         if self.bridges['remaining']() >= 1 \
                 and not 'Bridge' in self.config.config:
+
+            ## configure our first bridge line
             self.bridges['current'] = self.bridges['all'][0]
             self.config.Bridge = self.bridges['current']
                                                   ## avoid starting several
             self.config.save()                    ## processes 
-            assert self.config.config.has_key('Bridge'), "NO BRIDGE"
-
-            state = timeout(self.circuit_timeout)(start_tor(
-                    reactor, self.config, self.control_port, 
-                    self.tor_binary, self.data_directory))
-            state.addCallbacks(setup_done, setup_fail)
-            state.addCallback(remove_public_relays, self.bridges)
-
-            #controller = singleton_semaphore(bootstrap)
-            #controller = x().addCallback(singleton_semaphore, tor)
+            assert self.config.config.has_key('Bridge'), "No Bridge Line"
+
+            ## start tor and remove bridges which are public relays
+            from ooni.utils.onion import start_tor_filter_nodes
+            state = start_tor_filter_nodes(reactor, self.config, 
+                                           self.control_port, self.tor_binary,
+                                           self.data_directory, self.bridges)
+            #controller = defer.Deferred()
+            #controller.addCallback(singleton_semaphore, tor)
             #controller.addErrback(setup_fail)
+            #bootstrap = defer.gatherResults([controller, filter_bridges], 
+            #                                consumeErrors=True)
 
-            #filter_bridges = remove_public_relays(self.bridges)
-
-        #bootstrap = defer.gatherResults([controller, filter_bridges], 
-        #                                consumeErrors=True)
-        log.debug("Current callbacks on TorState():\n%s" % state.callbacks)
-        log.debug("TorState():\n%s" % state)
+            if state is not None:
+                log.debug("state:\n%s" % state)
+                log.debug("Current callbacks on TorState():\n%s" 
+                          % state.callbacks)
 
+        ## if we've got more bridges
         if self.bridges['remaining']() >= 2:
-            all = []
+            #all = []
             for bridge in self.bridges['all'][1:]:
                 self.bridges['current'] = bridge
                 #new = defer.Deferred()
-                new = defer.waitForDeferred(state)
-                new.addCallback(reconfigure_bridge, state, self.bridges)
-                all.append(new)
-
-        #state.chainDeferred(defer.DeferredList(all))
-        #state.chainDeferred(defer.gatherResults(all, consumeErrors=True))
-            check_remaining = defer.DeferredList(all, consumeErrors=True)
-
-            #controller.chainDeferred(check_remaining)
-            #log.debug("Current callbacks on TorState():\n%s" 
-            #          % controller.callbacks)
-            state.chainDeferred(check_remaining)
+                #new.addCallback(reconfigure_bridge, state, self.bridges)
+                #all.append(new)
+            #check_remaining = defer.DeferredList(all, consumeErrors=True)
+            #state.chainDeferred(check_remaining)
+                state.addCallback(reconfigure_bridge, self.bridges)
 
         if self.relays['remaining']() > 0:
             while self.relays['remaining']() >= 3:
@@ -434,16 +458,26 @@ class BridgetTest(OONITest):
                         continue
 
         #state.callback(all)
-        self.reactor.run()
+        #self.reactor.run()
         return state
 
     def startTest(self, args):
+        """
+        Local override of :meth:`OONITest.startTest` to bypass calling 
+        self.control.
+
+        :param args:
+            The current line of :class:`Asset`, not used but kept for 
+            compatibility reasons.
+        :return:
+            A fired deferred which callbacks :meth:`experiment` and 
+            :meth:`OONITest.finished`. 
+        """
         self.start_time = date.now()
-        self.laboratory = defer.Deferred()
-        self.laboratory.addCallbacks(self.experiment, errback=log.err)
-        self.laboratory.addCallbacks(self.finished, errback=log.err)
-        self.laboratory.callback(args)
-        return self.laboratory
+        self.d = self.experiment(args)
+        self.d.addErrback(log.err)
+        self.d.addCallbacks(self.finished, log.err)
+        return self.d
 
 ## So that getPlugins() can register the Test:
 bridget = BridgetTest(None, None, None)
@@ -453,11 +487,10 @@ bridget = BridgetTest(None, None, None)
 ## -----------
 ##
 ## TODO:
-##       o  cleanup documentation
+##       x  cleanup documentation
 ##       x  add DataDirectory option
 ##       x  check if bridges are public relays
 ##       o  take bridge_desc file as input, also be able to give same
 ##          format as output
 ##       x  Add asynchronous timeout for deferred, so that we don't wait 
 ##          forever for bridges that don't work.
-##       o  Add mechanism for testing through another host
diff --git a/ooni/utils/log.py b/ooni/utils/log.py
index 54c59ea..fe737f5 100644
--- a/ooni/utils/log.py
+++ b/ooni/utils/log.py
@@ -36,9 +36,9 @@ class OONITestFailure(Failure):
     Can be given an Exception as an argument, else will use the
     most recent Exception from the current stack frame.
     """
-    def __init__(self, exception=None, _type=None, 
+    def __init__(self, _type=None, 
                  _traceback=None, _capture=False):
-        Failure.__init__(self, exc_value=exception, exc_type=_type,
+        Failure.__init__(self, exc_type=_type,
                          exc_tb=_traceback, captureVars=_capture)
     
 class OONILogObserver(log.FileLogObserver):
@@ -96,7 +96,7 @@ def msg(message, level="info", **kw):
     log.msg(message, logLevel=level, **kw)
 
 def err(message, level="err", **kw):
-    log.err(message, logLevel=level, **kw)
+    log.err(logLevel=level, **kw)
 
-def fail(message, exception=None, level="crit", **kw):
+def fail(message, exception, level="crit", **kw):
     log.failure(message, OONITestFailure(exception, **kw), logLevel=level)
diff --git a/ooni/utils/onion.py b/ooni/utils/onion.py
index 6f6b355..3326c75 100644
--- a/ooni/utils/onion.py
+++ b/ooni/utils/onion.py
@@ -16,53 +16,22 @@
 # XXX TODO add report keys for onion methods
 
 import random
+import sys
 
-from ooni.lib.txtorcon import CircuitListenerMixin, IStreamAttacher
-from ooni.lib.txtorcon import TorState
-from ooni.utils        import log
 from twisted.internet  import defer
 from zope.interface    import implements
 
+from ooni.lib.txtorcon import CircuitListenerMixin, IStreamAttacher
+from ooni.lib.txtorcon import TorState, TorConfig
+from ooni.utils        import log
+from ooni.utils.timer  import deferred_timeout, TimeoutError
 
-def setup_done(proto):
-    log.msg("Setup Complete")
-    state = TorState(proto.tor_protocol)
-    state.post_bootstrap.addCallback(state_complete)
-    state.post_bootstrap.addErrback(setup_fail)
-
-def setup_fail(proto):
-    log.err("Setup Failed: %s" % proto)
-    #report.update({'setup_fail': proto})
-    reactor.stop()
-
-def state_complete(state):
-    """Called when we've got a TorState."""
-    log.msg("We've completely booted up a Tor version %s at PID %d"
-            % (state.protocol.version, state.tor_pid))
-    log.msg("This Tor has the following %d Circuits:"
-            % len(state.circuits))
-    for circ in state.circuits.values():
-        log.msg("%s" % circ)
-    return state
-
-def updates(_progress, _tag, _summary):
-    """Log updates on the Tor bootstrapping process.""" 
-    log.msg("%d%%: %s" % (_progress, _summary))
-
-def bootstrap(ctrl):
-    """
-    Bootstrap Tor from an instance of
-    :class:`ooni.lib.txtorcon.TorControlProtocol`.
-    """
-    conf = TorConfig(ctrl)
-    conf.post_bootstrap.addCallback(setup_done).addErrback(setup_fail)
-    log.msg("Tor process connected, bootstrapping ...")
 
 def parse_data_dir(data_dir):
     """
     Parse a string that a has been given as a DataDirectory and determine
     its absolute path on the filesystem.
-    
+
     :param data_dir:
         A directory for Tor's DataDirectory, to be parsed.
     :return:
@@ -90,7 +59,7 @@ def parse_data_dir(data_dir):
         assert path.isdir(data_dir), "Could not find %s" % data_dir
     except AssertionError, ae:
         log.err(ae)
-        sys.exit(1)
+        sys.exit()
     else:
         return data_dir
 
@@ -177,18 +146,55 @@ def remove_public_relays(state, bridges):
     if len(both) > 0:
         try:
             updated = map(lambda node: remove_node_from_list(node), both)
-            if not updated:
-                defer.returnValue(state)
-            else:
-                defer.returnValue(state)
+            log.debug("Bridges in both: %s" % both)
+            log.debug("Updated = %s" % updated)
+            #if not updated:
+            #    defer.returnValue(state)
+            #else:
+            #    defer.returnValue(state)
+            return state
         except Exception, e:
             log.err("Removing public relays %s from bridge list failed:\n%s"
                     % (both, e))
 
-#@defer.inlineCallbacks
+def setup_done(proto):
+    log.msg("Setup Complete")
+    state = TorState(proto.tor_protocol)
+    state.post_bootstrap.addCallback(state_complete)
+    state.post_bootstrap.addErrback(setup_fail)
+
+
+def setup_fail(proto):
+    log.msg("Setup Failed:\n%s" % proto)
+    return proto
+    #reactor.stop()
+
+def state_complete(state):
+    """Called when we've got a TorState."""
+    log.msg("We've completely booted up a Tor version %s at PID %d"
+            % (state.protocol.version, state.tor_pid))
+    log.msg("This Tor has the following %d Circuits:"
+            % len(state.circuits))
+    for circ in state.circuits.values():
+        log.msg("%s" % circ)
+    return state
+
+def updates(_progress, _tag, _summary):
+    """Log updates on the Tor bootstrapping process.""" 
+    log.msg("%d%%: %s" % (_progress, _summary))
+
+def bootstrap(ctrl):
+    """
+    Bootstrap Tor from an instance of
+    :class:`ooni.lib.txtorcon.TorControlProtocol`.
+    """
+    conf = TorConfig(ctrl)
+    conf.post_bootstrap.addCallback(setup_done).addErrback(setup_fail)
+    log.msg("Tor process connected, bootstrapping ...")
+
 def start_tor(reactor, config, control_port, tor_binary, data_dir,
-              report=None, progress=updates, process_cb=setup_done,
-              process_eb=setup_fail):
+              report=None, progress=updates, 
+              process_cb=None, process_eb=None):
     """
     Use a txtorcon.TorConfig() instance, config, to write a torrc to a
     tempfile in our DataDirectory, data_dir. If data_dir is None, a temp
@@ -212,7 +218,7 @@ def start_tor(reactor, config, control_port, tor_binary, data_dir,
     :param data_dir:
         The directory to use as Tor's DataDirectory.
     :param report:
-        The class:`ooni.plugoo.reports.Report` instance to .update().
+        The class:`ooni.plugoo.reports.Report` instance.
     :param progress:
         A non-blocking function to handle bootstrapping updates, which takes
         three parameters: _progress, _tag, and _summary.
@@ -252,40 +258,179 @@ def start_tor(reactor, config, control_port, tor_binary, data_dir,
     process_protocol = TorProcessProtocol(connection_creator, progress)
     process_protocol.to_delete = to_delete
 
+    if process_cb is not None and process_eb is not None:
+        process_protocol.connected_cb.addCallbacks(process_cb, process_eb)
+
     reactor.addSystemEventTrigger('before', 'shutdown',
                                   partial(delete_files_or_dirs, to_delete))
-    #try:
-    #    transport = yield reactor.spawnProcess(process_protocol,
-    #                                           tor_binary,
-    #                                           args=(tor_binary,'-f',torrc),
-    #                                           env={'HOME': data_dir},
-    #                                           path=data_dir)
-    #    if transport:
-    #        transport.closeStdin()
-    #except RuntimeError as e:
-    #    log.err("Starting Tor failed: %s" % e)
-    #    process_protocol.connected_cb.errback(e)
-    #except NotImplementedError, e:
-    #    url = "http://starship.python.net/crew/mhammond/win32/Downloads.html"
-    #    log.err("Running bridget on Windows requires pywin32: %s" % url)
-    #    process_protocol.connected_cb.errback(e)
     try:
-        transport = reactor.spawnProcess(process_protocol, 
-                                         tor_binary, 
+        transport = reactor.spawnProcess(process_protocol,
+                                         tor_binary,
                                          args=(tor_binary,'-f',torrc),
-                                         env={'HOME':data_dir},
+                                         env={'HOME': data_dir},
                                          path=data_dir)
         transport.closeStdin()
     except RuntimeError, e:
-        log.err(e)
-        process_protocol.connected_cb.errback(e)
+       log.err("Starting Tor failed:")
+       process_protocol.connected_cb.errback(e)
+    except NotImplementedError, e:
+       url = "http://starship.python.net/crew/mhammond/win32/Downloads.html"
+       log.msg("Running bridget on Windows requires pywin32: %s" % url)
+       process_protocol.connected_cb.errback(e)
 
     return process_protocol.connected_cb
 
-    #d = yield process_protocol.connected_cb
-    #defer.returnValue(d)
+ at defer.inlineCallbacks
+def start_tor_filter_nodes(reactor, config, control_port, tor_binary,
+                              data_dir, bridges):
+    """
+    Bootstrap a Tor process and return a fully-setup 
+    :class:`ooni.lib.txtorcon.TorState`. Then search for our bridges
+    to test in the list of known public relays, 
+    :ivar:`ooni.lib.txtorcon.TorState.routers`, and remove any bridges
+    which are known public relays.
+
+    :param reactor:
+        The :class:`twisted.internet.reactor`.
+    :param config:
+        An instance of :class:`ooni.lib.txtorcon.TorConfig`.
+    :param control_port:
+        The port to use for Tor's ControlPort. If already configured in
+        the TorConfig instance, this can be given as 
+        TorConfig.config.ControlPort.
+    :param tor_binary:
+        The full path to the Tor binary to execute.
+    :param data_dir:
+        The full path to the directory to use as Tor's DataDirectory.
+    :param bridges:
+        A dictionary which has a key 'all' which is a list of bridges to
+        test connecting to, e.g.:
+             bridges['all'] = ['1.1.1.1:443', '22.22.22.22:9001']
+    :return:
+        A fully initialized :class:`ooni.lib.txtorcon.TorState`.
+    """
+    setup = yield start_tor(reactor, config, control_port, 
+                            tor_binary, data_dir,
+                            process_cb=setup_done, process_eb=setup_fail)
+    filter_nodes = yield remove_public_relays(setup, bridges)
+    defer.returnValue(filter_nodes)
+
+ at defer.inlineCallbacks
+def start_tor_with_timer(reactor, config, control_port, tor_binary, data_dir,
+                         bridges, timeout):
+    """
+    Start bootstrapping a Tor process wrapped with an instance of the class
+    decorator :func:`ooni.utils.timer.deferred_timeout` and complete callbacks
+    to either :func:`setup_done` or :func:`setup_fail`. Return a fully-setup
+    :class:`ooni.lib.txtorcon.TorState`. Then search for our bridges to test
+    in the list of known public relays,
+    :ivar:`ooni.lib.txtorcon.TorState.routers`, and remove any bridges which
+    are listed as known public relays.
+
+    :param reactor:
+        The :class:`twisted.internet.reactor`.
+    :param config:
+        An instance of :class:`ooni.lib.txtorcon.TorConfig`.
+    :param control_port:
+        The port to use for Tor's ControlPort. If already configured in
+        the TorConfig instance, this can be given as 
+        TorConfig.config.ControlPort.
+    :param tor_binary:
+        The full path to the Tor binary to execute.
+    :param data_dir:
+        The full path to the directory to use as Tor's DataDirectory.
+    :param bridges:
+        A dictionary which has a key 'all' which is a list of bridges to
+        test connecting to, e.g.:
+             bridges['all'] = ['1.1.1.1:443', '22.22.22.22:9001']
+    :param timeout:
+        The number of seconds to attempt to bootstrap the Tor process before
+        raising a :class:`ooni.utils.timer.TimeoutError`.
+    :return:
+        If the timeout limit is not exceeded, return a fully initialized 
+        :class:`ooni.lib.txtorcon.TorState`, else return None.
+    """
+    error_msg = "Bootstrapping has exceeded the timeout limit..."
+    with_timeout = deferred_timeout(timeout, e=error_msg)(start_tor)
+    try:
+        setup = yield with_timeout(reactor, config, control_port, tor_binary,
+                                   data_dir, process_cb=setup_done,
+                                   process_eb=setup_fail)
+    except TimeoutError, te:
+        log.err(te)
+        defer.returnValue(None)
+    #except Exception, e:
+    #    log.err(e)
+    #    defer.returnValue(None)
+    else:
+        state = yield remove_public_relays(setup, bridges)
+        defer.returnValue(state)
+    
+ at defer.inlineCallbacks
+def start_tor_filter_nodes_with_timer(reactor, config, control_port, 
+                                      tor_binary, data_dir, bridges, timeout):
+    """
+    Start bootstrapping a Tor process wrapped with an instance of the class
+    decorator :func:`ooni.utils.timer.deferred_timeout` and complete callbacks
+    to either :func:`setup_done` or :func:`setup_fail`. Then, filter our list
+    of bridges to remove known public relays by calling back to
+    :func:`remove_public_relays`. Return a fully-setup
+    :class:`ooni.lib.txtorcon.TorState`. Then search for our bridges to test
+    in the list of known public relays,
+    :ivar:`ooni.lib.txtorcon.TorState.routers`, and remove any bridges which
+    are listed as known public relays.
+
+    :param reactor:
+        The :class:`twisted.internet.reactor`.
+    :param config:
+        An instance of :class:`ooni.lib.txtorcon.TorConfig`.
+    :param control_port:
+        The port to use for Tor's ControlPort. If already configured in
+        the TorConfig instance, this can be given as 
+        TorConfig.config.ControlPort.
+    :param tor_binary:
+        The full path to the Tor binary to execute.
+    :param data_dir:
+        The full path to the directory to use as Tor's DataDirectory.
+    :param bridges:
+        A dictionary which has a key 'all' which is a list of bridges to
+        test connecting to, e.g.:
+             bridges['all'] = ['1.1.1.1:443', '22.22.22.22:9001']
+    :param timeout:
+        The number of seconds to attempt to bootstrap the Tor process before
+        raising a :class:`ooni.utils.timer.TimeoutError`.
+    :return:
+        If the timeout limit is not exceeded, return a fully initialized 
+        :class:`ooni.lib.txtorcon.TorState`, else return None.
+    """
+    error_msg = "Bootstrapping has exceeded the timeout limit..."
+    with_timeout = deferred_timeout(timeout, e=error_msg)(start_tor_filter_nodes)
+    try:
+        state = yield with_timeout(reactor, config, control_port, 
+                                   tor_binary, data_dir, bridges)
+    except TimeoutError, te:
+        log.err(te)
+        defer.returnValue(None)
+    #except Exception, e:
+    #    log.err(e)
+    #    defer.returnValue(None)
+    else:
+        defer.returnValue(state)
     
 class CustomCircuit(CircuitListenerMixin):
+    """
+    Utility class for controlling circuit building. See 
+    'attach_streams_by_country.py' in the txtorcon documentation.
+
+    :param state:
+        A fully bootstrapped instance of :class:`ooni.lib.txtorcon.TorState`.
+    :param relays:
+        A dictionary containing a key 'all', which is a list of relays to 
+        test connecting to.
+    :ivar waiting_circuits:
+        The list of circuits which we are waiting to attach to. You shouldn't
+        need to touch this.
+    """
     implements(IStreamAttacher)
 
     def __init__(self, state, relays=None):
@@ -294,6 +439,15 @@ class CustomCircuit(CircuitListenerMixin):
         self.relays = relays
 
     def waiting_on(self, circuit):
+        """
+        Whether or not we are waiting on the given circuit before attaching to
+        it.
+
+        :param circuit:
+            An item from :ivar:`ooni.lib.txtorcon.TorState.circuits`.
+        :return:
+            True if we are waiting on the circuit, False if not waiting.
+        """
         for (circid, d) in self.waiting_circuits:
             if circuit.id == circid:
                 return True
@@ -318,6 +472,17 @@ class CustomCircuit(CircuitListenerMixin):
                 d.callback(circuit)
 
     def circuit_failed(self, circuit, reason):
+        """
+        If building a circuit has failed, try to remove it from our list of
+        :ivar:`waiting_circuits`, else request to build it.
+
+        :param circuit:
+            An item from :ivar:`ooni.lib.txtorcon.TorState.circuits`.
+        :param reason:
+            A :class:`twisted.python.fail.Failure` instance.
+        :return:
+            None
+        """
         if self.waiting_on(circuit):
             log.msg("Circuit %s failed for reason %s" % (circuit.id, reason))
             circid, d = None, None
@@ -335,22 +500,51 @@ class CustomCircuit(CircuitListenerMixin):
         """
         Check if a relay is a hop in one of our already built circuits. 
 
+        :param router:
+            An item from the list 
+            :func:`ooni.lib.txtorcon.TorState.routers.values()`.
         """
         for circ in self.state.circuits.values():
-            if router in circuit.path:
+            if router in circ.path:
             #router.update() ## XXX can i use without args? no.
                 TorInfo.dump(self)
 
     def request_circuit_build(self, deferred, path=None):
+        """
+        Request a custom circuit.
+
+        :param deferred:
+            A :class:`twisted.internet.defer.Deferred` for this circuit.
+        :param path:
+            A list of router ids to build a circuit from. The length of this
+            list must be at least three.
+        """
         if path is None:
-            if self.state.relays_remaining() > 0:
-                first, middle,last = (self.state.relays.pop()
-                                      for i in range(3))
+
+            pick   = self.relays['all'].pop
+            n      = self.state.entry_guards.values()
+            choose = random.choice
+
+            first, middle, last = (None for i in range(3))
+
+            if self.relays['remaining']() >= 3:
+                first, middle, last = (pick() for i in range(3))
+            elif self.relays['remaining']() < 3:
+                first = choose(n)
+                middle = pick()
+                if self.relays['remaining'] == 2:
+                    middle, last = (pick() for i in range(2))
+                elif self.relay['remaining'] == 1:
+                    middle = pick()
+                    last = choose(n)
+                else:
+                    log.msg("Qu'est-que fuque?")
             else:
-                first = random.choice(self.state.entry_guards.values())
                 middle, last = (random.choice(self.state.routers.values())
                                 for i in range(2))
+
             path = [first, middle, last]
+
         else:
             assert isinstance(path, list), \
                 "Circuit path must be a list of relays!"
@@ -379,7 +573,11 @@ class CustomCircuit(CircuitListenerMixin):
             log.err)
 
 class TxtorconImportError(ImportError):
-    """Raised when /ooni/lib/txtorcon cannot be imported from."""
+    """
+    Raised when ooni.lib.txtorcon cannot be imported from. Checks our current
+    working directory and the path given to see if txtorcon has been 
+    initialized via /ooni/lib/Makefile.
+    """
     from os import getcwd, path
 
     cwd, tx = getcwd(), 'lib/txtorcon/torconfig.py'
@@ -410,3 +608,82 @@ class PTNotFoundException(Exception):
         log.msg("%s" % m)
         return sys.exit()
 
+ at defer.inlineCallbacks
+def __start_tor_with_timer__(reactor, config, control_port, tor_binary, 
+                             data_dir, bridges=None, relays=None, timeout=None,
+                             retry=None):
+    """
+    A wrapper for :func:`start_tor` which wraps the bootstrapping of a Tor
+    process and its connection to a reactor with a 
+    :class:`twisted.internet.defer.Deferred` class decorator utility, 
+    :func:`ooni.utils.timer.deferred_timeout`, and a mechanism for resets.
+    
+    ## XXX fill me in
+    """
+    raise NotImplementedError
+    
+    class RetryException(Exception):
+        pass    
+
+    import sys
+    from ooni.utils.timer import deferred_timeout, TimeoutError
+    
+    def __make_var__(old, default, _type):
+        if old is not None:
+            assert isinstance(old, _type)
+            new = old
+        else:
+            new = default
+        return new
+    
+    reactor = reactor
+    timeout = __make_var__(timeout, 120, int)
+    retry   = __make_var__(retry, 1, int)
+
+    with_timeout = deferred_timeout(timeout)(start_tor)
+    
+    @defer.inlineCallbacks
+    def __start_tor__(rc=reactor, cf=config, cp=control_port, tb=tor_binary, 
+                      dd=data_dir, br=bridges, rl=relays, cb=setup_done,
+                      eb=setup_fail, af=remove_public_relays, retry=retry):
+        try:
+            setup = yield with_timeout(rc,cf,cp,tb,dd)
+        except TimeoutError:
+            retry -= 1
+            defer.returnValue(retry)
+        else:
+            if setup.callback:
+                setup = yield cb(setup)
+            elif setup.errback:
+                setup = yield eb(setup)
+            else:
+                setup = setup
+    
+            if br is not None:
+                state = af(setup,br)
+            else:
+                state = setup
+            defer.returnValue(state)
+    
+    @defer.inlineCallbacks
+    def __try_until__(tries):
+        result = yield __start_tor__()
+        try:
+            assert isinstance(result, int)
+        except AssertionError:
+            defer.returnValue(result)
+        else:
+            if result >= 0:
+                tried = yield __try_until__(result)
+                defer.returnValue(tried)
+            else:
+                raise RetryException
+    try:
+        tried = yield __try_until__(retry)
+    except RetryException:
+        log.msg("All retry attempts to bootstrap Tor have timed out.")
+        log.msg("Exiting ...")
+        defer.returnValue(sys.exit())
+    else:
+        defer.returnValue(tried)
+
diff --git a/ooni/utils/timer.py b/ooni/utils/timer.py
index 24074f9..e03fd74 100644
--- a/ooni/utils/timer.py
+++ b/ooni/utils/timer.py
@@ -17,17 +17,24 @@ def timeout(seconds, e=None):
     """
     A decorator for blocking methods to cause them to timeout. Can be used
     like this: 
+
         @timeout(30)
-        def foo():
+        def foo(arg1, kwarg="baz"):
             for x in xrange(1000000000):
+                print "%s %s" % (arg1, kwarg)
                 print x
+
     or like this:
-        ridiculous = timeout(30)(foo)
+
+        ridiculous = timeout(30)(foo("bar"))
 
     :param seconds:
         Number of seconds to wait before raising :class:`TimeoutError`.
     :param e:
         Error message to pass to :class:`TimeoutError`. Default None.
+    :return:
+        The result of the original function, or else an instance of 
+        :class:`TimeoutError`.
     """
     from signal    import alarm, signal, SIGALRM
     from functools import wraps
@@ -45,3 +52,56 @@ def timeout(seconds, e=None):
             return res
         return wraps(func)(wrapper)
     return decorator
+
+def deferred_timeout(seconds, e=None):
+    """
+    Decorator for adding a timeout to an instance of a
+    :class:`twisted.internet.defer.Deferred`. Can be used like this:
+
+        @deferred_timeout(30)
+        def foo(arg1, kwarg="baz"):
+            for x in xrange(1000000000):
+                print "%s %s" % (arg1, kwarg)
+                print x
+
+    or like this:
+
+        ridiculous = deferred_timeout(30)(foo("bar"))
+
+    :param seconds:
+        Number of seconds to wait before raising :class:`TimeoutError`.
+    :param e:
+        Error message to pass to :class:`TimeoutError`. Default None.
+    :return:
+        The result of the orginal :class:`twisted.internet.defer.Deferred`
+        or else a :class:`TimeoutError`.
+    """
+    from twisted.internet import defer, reactor
+
+    def wrapper(func):
+        @defer.inlineCallbacks
+        def _timeout(*args, **kwargs):
+            d_original = func(*args, **kwargs)
+            if not isinstance(d_original, defer.Deferred):
+                defer.returnValue(d_original) ## fail gracefully
+            d_timeout = defer.Deferred()
+            timeup = reactor.callLater(seconds, d_timeout.callback, None)
+            try:
+                original_result, timeout_result = \
+                    yield defer.DeferredList([d_original, d_timeout],
+                                             fireOnOneCallback=True,
+                                             fireOnOneErrback=True,
+                                             consumeErrors=True)
+            except defer.FirstError, dfe:
+                assert dfe.index == 0         ## error in original
+                timeup.cancel()
+                dfe.subFailure.raiseException()
+            else:
+                if d_timeout.called:          ## timeout
+                    d_original.cancel()
+                    raise TimeoutError, e
+            timeup.cancel()                   ## no timeout
+            defer.returnValue(d_original)
+        return _timeout
+    return wrapper
+





More information about the tor-commits mailing list