Search code examples
pythoncallbacktwisted

weird behaviour in twisted defer callback


Aim : To try a TCP connection to a list of server and print if the connection was successful and prompted for password or not

Problem : It seems as if a statement(to_check -= 1) in my callback(named connected) and errback(named failed) never gets executed, even though the print statements in those functions are.

from twisted.internet import protocol, defer
import sys

class myProto(protocol.Protocol):
    got = ''
    def dataReceived(self,data):
        #print data
        self.got += data
        if "Password:" in data:
            self.transport.loseConnection()

    def connectionLost(self,reason):
        #print self.got
        if "Password:" in self.got:
            self.factory.success("and was prompted for password")
        else:
            self.factory.success("But was not prompted for password")

class myFactory(protocol.ClientFactory):
    protocol = myProto

    def __init__(self,deferred,host):
        self.deferred = deferred
        self.host = host
        print "Trying Connection to %s ..." % host

    def success(self,askpass):
        if self.deferred is not None:
            d, self.deferred = self.deferred , None
            d.callback([self.host,askpass])

    def clientConnectionFailed(self, connector, reason):
        if self.deferred is not None:
            d, self.deferred = self.deferred, None
            d.errback(reason)

def check_conn(host, port):
    from twisted.internet import reactor
    d = defer.Deferred()
    factory = myFactory(d,host)
    reactor.connectTCP(host,port,factory)
    return d

def main():

    ip = "10.1.1."
    port = 23
    total = 10
    to_check = total
    from twisted.internet import reactor
    def connected(whathappened):
        print >>sys.stdout, "Successfully connected to %s %s" % (whathappened[0],whathappened[1])
        to_check -= 1

    def failed(reason):
        print >>sys.stderr, "Connection to failed : %s" % reason
        to_check -= 1

    def checked(_):
        print >>sys.stdout, "%d connections left to check" % to_check
        if to_check == 0:
            reactor.stop()

    for i in range(0,total):
        d = check_conn(ip + str(i),port)
        d.addCallbacks(connected,failed)
        d.addBoth(checked)

    reactor.run()

if __name__ == "__main__":
    main()

Output :

Trying Connection to 10.1.1.0 ...
...
...
Trying Connection to 10.1.1.9 ...
Successfully connected to 10.1.1.1 and was prompted for password
10 connections left to check
Successfully connected to 10.1.1.0 and was prompted for password
10 connections left to check
Successfully connected to 10.1.1.2 and was prompted for password
10 connections left to check
Successfully connected to 10.1.1.9 and was prompted for password
10 connections left to check
....{Similar output}
Successfully connected to 10.1.1.6 and was prompted for password
10 connections left to check

Number of connection left to check should be decreasing, but it remains the same.


Solution

  • This is a general issue with your understanding of closures in python; by default, variables are local to the innermost function in which they are assigned. -= is an implicit assignment, so to_check becomes a local variable to connected and failed. As such, the to_check in the main function is never changed. In python 3.x, nonlocal to_check at the top of connected and failed would make it do what you expect. Here's an example of how to accomplish the same thing using mutation in 2.x:

    import itertools
    
    def main():
    
        ip = "10.1.1."
        port = 23
        to_check = 10
        counter = itertools.count().next
        from twisted.internet import reactor
        def connected(whathappened):
            print >>sys.stdout, "Successfully connected to %s %s" % (whathappened[0],whathappened[1])
            return counter()
    
        def failed(reason):
            print >>sys.stderr, "Connection to failed : %s" % reason
            return counter()
    
        def checked(count):
            print >>sys.stdout, "%d connections left to check" % (to_check - count,)
            if count == to_check:
                reactor.stop()
    
        for i in range(0,total):
            d = check_conn(ip + str(i),port)
            d.addCallbacks(connected,failed)
            d.addBoth(checked)
    
        reactor.run()