#!/usr/bin/env python

##
## This example uses ICircuitListener to monitor how many circuits have
## failed since the monitor started up. If this figure is more than 50%,
## a warning-level message is logged.
## 
## Like the :ref:`stream_circuit_logger.py` example, we also log all new
## circuits.
## 

import os
import sys
import random
import time

from twisted.internet import reactor, task, endpoints
from twisted.python import usage
from zope.interface import implements

import txtorcon

class Options(usage.Options):
    """
    command-line options we understand
    """
    
    optParameters = [
        ['failed', 'f', 0, 'Starting value for number of failed circuits.'],
        ['built', 'b', 0, 'Starting value for the total number of built cicuits.'],
        ['connect', 'c', None, 'Tor control socket to connect to in host:port format, like "localhost:9051" (the default).']
        ]

    def __init__(self):
        usage.Options.__init__(self)
        self['guards'] = []

    def opt_guard(self, value):
        name, built, failed = value.split(',')
        self['guards'].append((name, int(built), int(failed)))
        

class CircuitFailureWatcher(txtorcon.CircuitListenerMixin):

    built_circuits = 0
    failed_circuits = 0
    percent = 0.0
    failed_circuit_ids = []
    per_guard_built = {}
    per_guard_failed = {}

    def print_update(self):
        print time.ctime(reactor.seconds()) + ': ' + self.information()

    def update_percent(self):
        self.percent = 100.0 * (float(self.failed_circuits) / float(self.built_circuits + self.failed_circuits))
        if self.percent > 50.0:
            print 'WARNING: %02.1f percent of all routes have failed: %d failed, %d built' % (self.percent, self.failed_circuits, self.built_circuits)

    def information(self):
        rtn = '%02.1f%% of all circuits have failed: %d failed, %d built' % (self.percent, self.failed_circuits, self.built_circuits)
        for g in self.per_guard_built.keys():
            per_guard_percent = 100.0*(self.per_guard_failed[g]/(self.per_guard_built[g]+self.per_guard_failed[g]))
            rtn = rtn + '\n  %s: %d built, %d failed: %02.1f%%' % (g, self.per_guard_built[g], self.per_guard_failed[g],
                                                                   per_guard_percent)
        return rtn

    def circuit_built(self, circuit):
        """ICircuitListener API"""
        if circuit.purpose == 'GENERAL':
            if len(circuit.path) > 0 and circuit.path[0] not in self.state.entry_guards.values():
                print "WEIRD: first circuit hop not in entry guards:",circuit,circuit.path
                return
            
            self.built_circuits += 1
            self.update_percent()

            if len(circuit.path) != 3 and len(circuit.path) != 4:
                print "WEIRD: circuit has odd pathlength:",circuit,circuit.path
            try:
                self.per_guard_built[circuit.path[0].unique_name] += 1
            except KeyError:
                self.per_guard_built[circuit.path[0].unique_name] = 1.0
                self.per_guard_failed[circuit.path[0].unique_name] = 0.0
        
    def circuit_failed(self, circuit, reason):
        """ICircuitListener API"""
        if circuit.purpose == 'GENERAL':
            if len(circuit.path) > 1 and circuit.path[0] not in self.state.entry_guards.values():
                ## note that single-hop circuits are built for various
                ## internal reasons (and it seems they somtimes use
                ## GENERAL anyway)
                print "WEIRD: first circuit hop not in entry guards:",circuit,circuit.path
                return
            
            self.failed_circuits += 1
            print "failed",circuit.id
            if not circuit.id in self.failed_circuit_ids:
                self.failed_circuit_ids.append(circuit.id)
            else:
                print "WARNING: duplicate message for",circuit

            if len(circuit.path) > 0:
                try:
                    self.per_guard_failed[circuit.path[0].unique_name] += 1
                except KeyError:
                    self.per_guard_failed[circuit.path[0].unique_name] = 1.0
                    self.per_guard_built[circuit.path[0].unique_name] = 0.0
                
            self.update_percent()

listener = CircuitFailureWatcher()
def setup(state):
    print 'Connected to a Tor version %s' % state.protocol.version
    global options, listener

    listener.failed_circuits = int(options['failed'])
    listener.built_circuits = int(options['built'])
    listener.state = state              # FIXME use ctor (ditto for options, probably)
    for name, built, failed in options['guards']:
        listener.per_guard_built[name] = built
        listener.per_guard_failed[name] = failed
    
    for circ in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()):
        if circ.state == 'BUILT':
            listener.circuit_built(circ)
    state.add_circuit_listener(listener)
    # print an update every minute
    task.LoopingCall(listener.print_update).start(60.0)

def setup_failed(arg):
    print "SETUP FAILED",arg
    print arg
    reactor.stop()

options = Options()
try:
    options.parseOptions(sys.argv[1:])
except usage.UsageError:
    print "This monitors circuit failure rates on multi-hop PURPOSE_GENERAL circuits only."
    print "Tor internally uses other circuit types or GENERAL single-hop circuits for"
    print "internal use and we try to ignore these."
    print
    print "Every minute, the summary is printed out. For each entry-guard your Tor is"
    print "currently using, a separate count and summary is printed."
    print
    print "Nothing is saved to disc. If you wish to start again with the same totals"
    print "as a previous run, use the options below. On exit, a command-line suitable"
    print "to do this is printed."
    print
    print options.getUsage()
    sys.exit(-1)

def on_shutdown(*args):
    global listener
    print '\nTo carry on where you left off, run:'
    print '  %s --failed %d --built %d' % (sys.argv[0], listener.failed_circuits, listener.built_circuits),
    for name in listener.per_guard_built.keys():
        print '--guard %s,%d,%d' % (name, listener.per_guard_built[name], listener.per_guard_failed[name]),
    print
reactor.addSystemEventTrigger('before', 'shutdown', on_shutdown)

if options['connect']:
    host, port = options['connect'].split(':')
    port = int(port)
    print "Connecting to %s:%d..." % (host, port)
    endpoint = endpoints.clientFromString(reactor, 'tcp:host=%s:port=%d' % (host, port))
    
else:
    print "Connecting to localhost:9051..."
    endpoint = endpoints.TCP4ClientEndpoint(reactor, "localhost", 9051)
    
d = txtorcon.build_tor_connection(endpoint, build_state=True)
d.addCallback(setup).addErrback(setup_failed)
reactor.run()

@@ -670,24 +670,29 @@
 d circuits.'
+, int
 %5D,%0A        %5B
@@ -763,16 +763,21 @@
 icuits.'
+, int
 %5D,%0A     
@@ -895,16 +895,93 @@
 ault).'%5D
+,%0A        %5B'delay', 'n', 60, 'Seconds to wait between status updates.', int%5D,
 %0A       
@@ -5190,12 +5190,24 @@
 art(
-60.0
+options%5B'delay'%5D
 )%0A%0Ad

add option for delay, better parsing