Michael Ivko
Michael Ivko

Reputation: 1292

Twisted doesn't exit when reactor.stop is called

I am trying to reimplement netcat in Python:

#!/usr/bin/env python2

from sys import stdin, stdout

from twisted.internet import reactor
from twisted.internet.protocol import Protocol
from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol

class NcClient(Protocol):
    def dataReceived(self, data):
            stdout.write(data)

    def sendData(self, data):
        self.transport.write(data)
        self.transport.write("\n")

client = NcClient()

def cmdloop():
    while True:
        line = stdin.readline()
        if line == "":
            break
        else:
            client.sendData(line)
    if reactor.running:
        reactor.stop()

point = TCP4ClientEndpoint(reactor, "localhost", 6004)
connectProtocol(point, client)
reactor.callInThread(cmdloop)
reactor.run()

When cmdloop detects end of input, it calls reactor.stop. As I understand, reactor.stop sends shutdown events to all things managed by Twisted, such as threads, connections etc. In response to these events connections are closed, threads wait for the completion of their procedures etc. So when reactor.stop() is called, the connection to localhost:6004 should close, and the program should exit.

However, it doesn't happen immediately, but only when NcClient receives a message from server. As if it is reading those messages blockingly, in a loop, and only when it receives one does it proceed to handle shutdown requests.

How to make it shut down before receiving a message? I know of reactor.crash(), but is there a more polite option?

Upvotes: 2

Views: 2774

Answers (1)

Glyph
Glyph

Reputation: 31910

Your main problem is that cmdloop is running in a non-reactor thread, and yet it is calling reactor methods other than callFromThread (specifically: transport.write via client.sendData). The documentation is quite clear on this:

Methods within Twisted may only be invoked from the reactor thread unless otherwise noted. Very few things within Twisted are thread-safe.

Luckily, implementing the equivalent of netcat doesn't require threading at all. You can simply use Twisted's built-in support for standard I/O as a source of data. Here's an example version:

import sys

from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory
from twisted.internet.endpoints import clientFromString
from twisted.internet.stdio import StandardIO

class NcClient(Protocol):
    def __init__(self, forwardTo):
        self.forwardTo = forwardTo

    def connectionMade(self):
        self.transport.registerProducer(self.forwardTo.transport, True)
        self.forwardTo.transport.resumeProducing()
        self.forwardTo.transport.registerProducer(self.transport, True)

    def dataReceived(self, data):
        self.forwardTo.transport.write(data)

    def connectionLost(self, reason):
        reactor.stop()

class StdIo(Protocol):
    def connectionMade(self):
        self.transport.pauseProducing()
        f4p = Factory.forProtocol(lambda: NcClient(self))
        d = endpoint.connect(f4p)
        @d.addCallback
        def connected(proto):
            self.client = proto

    def dataReceived(self, data):
        self.client.transport.write(data)

    def connectionLost(self, reason):
        reactor.stop()

endpoint = clientFromString(reactor, sys.argv[1])

output = StandardIO(proto=StdIo(), reactor=reactor)

reactor.run()

If you still want to use threads for some reason, you can modify NcClient to use callFromThread to invoke sendData instead of calling it directly.

Upvotes: 2

Related Questions