Reputation: 1292
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
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