As you all already know TwistedMatrix is great to write asynchronous event-driven network oriented programs: define how your protocol responds in case of events, attach some callbacks if you need them, wrap it in a factory and activate the reactor.
The reactor runs a giant loop in which events are processed in a non-blocking fashion. Sometimes, though, everything a man needs it’s just to make it stop. At least for a while, at least for the sake of getting data from the user.
The most prominent example of a command line client program that needs to stop and wait is a REPL. The following kind of REPL is a bit unorthodox, you’ll see.
The secret sauce (at least the one I found) to write such kind of interactive program in Twisted is twisted.internet.stdio.StandardIO. It connects your protocol to standard input and output:
class Repl(basic.LineReceiver): delimiter = '\n' prompt_string = 'cmd> ' def prompt(self): self.transport.write(self.prompt_string) def connectionMade(self): self.sendLine('Welcome to Console') self.prompt() def lineReceived(self, line): # blank line if not line: self.prompt() return self.issueCommand(line) def issueCommand(self, command): # send the command to the server d = sendCmd("%s%s" % (command, self.delimiter)) d.addCallback(self._checkResponse) def _checkResponse(self, args): success, num_lines, data = args if num_lines > 20: # use less to display the response self.lessify(data) else: self.sendLine(data) self.prompt() def lessify(self, data): p = subprocess.Popen(["less"], stdin=subprocess.PIPE) p.communicate(data) def connectionLost(self, reason): reactor.stop()
When the protocol it is connected to the standard output with
stdio.StandardIO(TaskConsole()) reactor.run()
the program displays the prompt, hence when a line is received from the standard input it is sent to the other protocol attached on the network and a callback is registered for the response. I also decided to use less to display the response if it’s more than some lines but that’s a detail.
The sendCmd function instantiate the networked protocol and its factory:
def sendCmd(cmd): factory = CFactory(cmd) reactor.connectTCP('127.0.0.1', 1234, factory) return factory.deferred
Then, when the server replies with some content we check to see if everything went ok and the reconstruct the whole response sending it back the REPL:
class Client(basic.LineReceiver): delimiter = '\n' def connectionMade(self): # send the command received by the cmdline to the server self.sendLine(self.factory.cmd) self.buffer = [] self.cmd_success = True def lineReceived(self, line): # basic check error/success if line.startswith('OK'): return if line.startswith('ERR'): self.cmd_success = False return if line == 'END': # join the response at the end of it self.responseFinished( len(self.buffer), "\n".join(self.buffer)) self.buffer = [] else: self.buffer.append(line) def responseFinished(self, num_lines, data): # disconnect self.sendLine('quit') self.transport.loseConnection() # send back the response to the REPL self.factory.deferred.callback(( self.cmd_success, num_lines, data)) class CFactory(protocol.ClientFactory): protocol = Client def __init__(self, cmd): self.cmd = cmd self.deferred = defer.Deferred()
How cool is that? Not really to be honest. It has a big gigantic fault: every time you input a line a connection to the server is opened and closed. That’s bad, really bad.
It didn’t take too long to create a version that use just one connection:
def connectionMade(self): self.sendLine('Welcome to Console') self.factory = CFactory() self.connector = reactor.connectTCP( '127.0.0.1', 1234, self.factory) self.prompt()
We store the connector and the factory. issueCommand does not open a connection anymore, just:
def issueCommand(self, command): self.connector.transport.write("%s%s" % (command, self.delimiter)) self.factory.deferred.addCallback(self._checkResponse)
We write directly to the transport of the connector (and not the one connected to the stdout) and register the callback on the factory’s deferred.
That’s better in my opinion and a nice start. I know that within twisted.conch.stdio there’s something more evolved. I’ll try to look into it when I have more time.
You can find the first version (bad) and the second version (better) online.
What do you think about this try?

