from twisted.internet import reactor reactor.run()
Here, we just run the reactor. Nothing at all will happen, until we interrupt the program. It will not consume (almost) no CPU resources. Not very useful, perhaps -- but this is the skeleton inside which the Twisted program will grow.
from twisted.internet import protocol, reactor class FingerProtocol(protocol.Protocol): pass class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol reactor.listenTCP(1079, FingerFactory()) reactor.run()
Here, we start listening on port 1079 [which is supposed to be a reminder that eventually, we want to run on port 79, the port the finger server is supposed to run on. We define a protocol which does not respond to any events. Thus, connections to 1079 will be accepted, but the input ignored.
from twisted.internet import protocol, reactor class FingerProtocol(protocol.Protocol): def connectionMade(self): self.transport.loseConnection() class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol reactor.listenTCP(1079, FingerFactory()) reactor.run()
Here we add to the protocol the ability to respond to the event of beginning a connection -- by terminating it. Perhaps not an interesting behaviour, but it is already not that far from behaving according to the letter of the protocol. After all, there is no requirement to send any data to the remote connection in the standard, is there. The only technical problem is that we terminate the connection too soon. A client which is slow enough will see his send() of the username result in an error.
from twisted.internet import protocol, reactor from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.transport.loseConnection() class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol reactor.listenTCP(1079, FingerFactory()) reactor.run()
Here we make FingerProtocol
inherit from
LineReceiver
, so that we get data-based events
on a line-by-line basis. We respond to the event of receiving
the line with shutting down the connection. Congratulations,
this is the first standard-compliant version of the code.
However, usually people actually expect some data about
users to be transmitted.
from twisted.internet import protocol, reactor from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.transport.write("No such user\r\n") self.transport.loseConnection() class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol reactor.listenTCP(1079, FingerFactory()) reactor.run()
Finally, a useful version. Granted, the usefulness is somewhat limited by the fact that this version only prints out a no such user message. It could be used for devestating effect in honeypots, of course :)
# Read username, output from empty factory, drop connections from twisted.internet import protocol, reactor from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.transport.write(self.factory.getUser(user)+"\r\n") self.transport.loseConnection() class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol def getUser(self, user): return "No such user" reactor.listenTCP(1079, FingerFactory()) reactor.run()
The same behaviour, but finally we see what usefuleness the factory has: as something that does not get constructed for every connection, it can be in charge of the user database. In particular, we won't have to change the protocol if the user database backend changes.
# Read username, output from non-empty factory, drop connections from twisted.internet import protocol, reactor from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.transport.write(self.factory.getUser(user)+"\r\n") self.transport.loseConnection() class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol def __init__(self, **kwargs): self.users = kwargs def getUser(self, user): return self.users.get(user, "No such user") reactor.listenTCP(1079, FingerFactory(moshez='Happy and well')) reactor.run()
Finally, a really useful finger database. While it does not supply information about logged in users, it could be used to distribute things like office locations and internal office numbers. As hinted above, the factory is in charge of keeping the user database: note that the protocol instance has not changed. This is starting to look good: we really won't have to keep tweaking our protocol.
# Read username, output from non-empty factory, drop connections # Use deferreds, to minimize synchronicity assumptions from twisted.internet import protocol, reactor, defer from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol def __init__(self, **kwargs): self.users = kwargs def getUser(self, user): return defer.succeed(self.users.get(user, "No such user")) reactor.listenTCP(1079, FingerFactory(moshez='Happy and well')) reactor.run()
But, here we tweak it just for the hell of it. Yes, while the previous version worked, it did assume the result of getUser is always immediately available. But what if instead of an in memory database, we would have to fetch result from a remote Oracle? Or from the web? Or, or...
# Read username, output from factory interfacing to OS, drop connections from twisted.internet import protocol, reactor, defer, utils from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol def getUser(self, user): return utils.getProcessOutput("finger", [user]) reactor.listenTCP(1079, FingerFactory()) reactor.run()
...from running a local command? Yes, this version (safely!) runs finger locally with whatever arguments it is given, and returns the standard output. This will do exactly what the standard version of the finger server does -- without the need for any remote buffer overflows, as the networking is done safely.
# Read username, output from factory interfacing to web, drop connections from twisted.internet import protocol, reactor, defer, utils from twisted.protocols import basic from twisted.web import client class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol def __init__(self, prefix): self.prefix=prefix def getUser(self, user): return client.getPage(self.prefix+user) reactor.listenTCP(1079, FingerFactory(prefix='http://livejournal.com/~')) reactor.run()
The web. That invention which has infiltrated homes around the world finally gets through to our invention. Here we use the built-in Twisted web client, which also returns a deferred. Finally, we manage to have examples of three different database backends, which do not change the protocol class. In fact, we will not have to change the protocol again until the end of this talk: we have achieved, here, one truly usable class.
# Read username, output from non-empty factory, drop connections # Use deferreds, to minimize synchronicity assumptions # Write application. Save in 'finger.tpy' from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol def __init__(self, **kwargs): self.users = kwargs def getUser(self, user): return defer.succeed(self.users.get(user, "No such user")) application = app.Application('finger', uid=1, gid=1) application.listenTCP(79, FingerFactory(moshez='Happy and well'))
Up until now, we faked. We kept using port 1079, because really, who wants to run a finger server with root privileges? Well, the common solution is "privilege shedding": after binding to the network, become a different, less privileged user. We could have done it ourselves, but Twisted has a builtin way to do it. Create a snippet as above, defining an application object. That object will have uid and gid attributes. When running it (later we will see how) it will bind to ports, shed privileges and then run.
root% twistd -ny finger.tpy # just like before root% twistd -y finger.tpy # daemonize, keep pid in twistd.pid root% twistd -y finger.tpy --pidfile=finger.pid root% twistd -y finger.tpy --rundir=/ root% twistd -y finger.tpy --chroot=/var root% twistd -y finger.tpy -l /var/log/finger.log root% twistd -y finger.tpy --syslog # just log to syslog root% twistd -y finger.tpy --syslog --prefix=twistedfinger # use given prefix
This is how to run "Twisted Applications" -- files which define an 'application'. twistd (TWISTed Daemonizer) does everything a daemon can be expected to -- shuts down stdin/stdout/stderr, disconnects from the terminal and can even change runtime directory, or even the root filesystems. In short, it does everything so the Twisted application developer can concentrate on writing his networking code.
# But let's try and fix setting away messages, shall we? from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerFactory(protocol.ServerFactory): protocol = FingerProtocol def __init__(self, **kwargs): self.users = kwargs def getUser(self, user): return defer.succeed(self.users.get(user, "No such user")) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): self.factory.setUser(*self.lines) class FingerSetterFactory(protocol.ServerFactory): def __init__(self, ff): self.setUser = self.ff.users.__setitem__ ff = FingerFactory(moshez='Happy and well') fsf = FingerSetterFactory(ff) application = app.Application('finger', uid=1, gid=1) application.listenTCP(79, ff) application.listenTCP(1079, fsf, interface='127.0.0.1')
Now that port 1079 is free, maybe we can run on it a different server, one which will let people set their messages. It does no access control, so anyone who can login to the machine can set any message. We assume this is the desired behaviour in our case. Testing it can be done by simply:
% nc localhost 1079 moshez Giving a talk now, sorry! ^D
# Fix asymmetry from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): self.factory.setUser(*self.lines) class FingerService(app.ApplicationService): def __init__(self, *args, **kwargs): app.ApplicationService.__init__(self, *args) self.users = kwargs def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getFingerFactory(self): f = protocol.ServerFactory() f.protocol, f.getUser = FingerProtocol, self.getUser return f def getFingerSetterFactory(self): f = protocol.ServerFactory() f.protocol, f.setUser = FingerSetterProtocol, self.users.__setitem__ return f application = app.Application('finger', uid=1, gid=1) f = FingerService(application, 'finger', moshez='Happy and well') application.listenTCP(79, f.getFingerFactory()) application.listenTCP(1079, f.getFingerSetterFactory(), interface='127.0.0.1')
The previous version had the setter poke at the innards of the finger factory. It's usually not a good idea: this version makes both factories symmetric by making them both look at a single object. Services are useful for when an object is needed which is not related to a specific network server. Here, we moved all responsibility for manufacturing factories into the service. Note that we stopped subclassing: the service simply puts useful methods and attributes inside the factories. We are getting better at protocol design: none of our protocol classes had to be changed, and neither will have to change until the end of the talk.
# Read from file from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): self.factory.setUser(*self.lines) class FingerService(app.ApplicationService): def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getFingerFactory(self): f = protocol.ServerFactory() f.protocol, f.getUser = FingerProtocol, self.getUser return f application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, f.getFingerFactory())
This version shows how, instead of just letting users set their messages, we can read those from a centrally managed file. We cache results, and every 30 seconds we refresh it. Services are useful for such scheduled tasks.
# Read from file, announce on the web! from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic from twisted.web import resource, server, static import cgi class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): self.factory.setUser(*self.lines) class FingerService(app.ApplicationService): def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getFingerFactory(self): f = protocol.ServerFactory() f.protocol, f.getUser = FingerProtocol, self.getUser return f def getResource(self): r = resource.Resource() r.getChild = (lambda path, request: static.Data('text/html', '<h1>%s</h1><p>%s</p>' % tuple(map(cgi.escape, [path,self.users.get(path, "No such user")])))) application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, f.getFingerFactory()) application.listenTCP(80, server.Site(f.getResource()))
The same kind of service can also produce things useful for other protocols. For example, in twisted.web, the factory itself (the site) is almost never subclassed -- instead, it is given a resource, which represents the tree of resources available via URLs. That hierarchy is navigated by site, and overriding it dynamically is possible with getChild.
# Read from file, announce on the web, irc from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic, irc from twisted.web import resource, server, static import cgi class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): self.factory.setUser(*self.lines) class IRCReplyBot(irc.IRCClient): def connectionMade(self): self.nickname = self.factory.nickname irc.IRCClient.connectionMade(self) def privmsg(self, user, channel, msg): if user.lower() == channel.lower(): self.factory.getUser(msg ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: self.msg(user, m)) class FingerService(app.ApplicationService): def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getFingerFactory(self): f = protocol.ServerFactory() f.protocol, f.getUser = FingerProtocol, self.getUser return f def getResource(self): r = resource.Resource() r.getChild = (lambda path, request: static.Data('text/html', '<h1>%s</h1><p>%s</p>' % tuple(map(cgi.escape, [path,self.users.get(path, "No such user")])))) def getIRCBot(self, nickname): f = protocol.ReconnectingClientFactory() f.protocol,f.nickname,f.getUser = IRCReplyBot,nickname,self.getUser return f application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, f.getFingerFactory()) application.listenTCP(80, server.Site(f.getResource())) application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
This is the first time there is client code. IRC clients often act a lot like servers: responding to events form the network. The reconnecting client factory will make sure that severed links will get re-established, with intelligent tweaked exponential backoff algorithms. The irc client itself is simple: the only real hack is getting the nickname from the factory in connectionMade.
# Read from file, announce on the web, irc, xml-rpc from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic, irc from twisted.web import resource, server, static, xmlrpc import cgi class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): self.factory.getUser(user ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: (self.transport.write(m+"\r\n"),self.transport.loseConnection())) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): self.factory.setUser(*self.lines) class IRCReplyBot(irc.IRCClient): def connectionMade(self): self.nickname = self.factory.nickname irc.IRCClient.connectionMade(self) def privmsg(self, user, channel, msg): if user.lower() == channel.lower(): self.factory.getUser(msg ).addErrback(lambda _: "Internal error in server" ).addCallback(lambda m: self.msg(user, m)) class FingerService(app.ApplicationService): def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getFingerFactory(self): f = protocol.ServerFactory() f.protocol, f.getUser = FingerProtocol, self.getUser return f def getResource(self): r = resource.Resource() r.getChild = (lambda path, request: static.Data('text/html', '<h1>%s</h1><p>%s</p>' % tuple(map(cgi.escape, [path,self.users.get(path, "No such user")])))) x = xmlrpc.XMLRPRC() x.xmlrpc_getUser = self.getUser r.putChild('RPC2.0', x) return r def getIRCBot(self, nickname): f = protocol.ReconnectingClientFactory() f.protocol,f.nickname,f.getUser = IRCReplyBot,nickname,self.getUser return f application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, f.getFingerFactory()) application.listenTCP(80, server.Site(f.getResource())) application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
In Twisted, XML-RPC support is handled just as though it was another resource. That resource will still support GET calls normally through render(), but that is usually left unimplemented. Note that it is possible to return deferreds from XML-RPC methods. The client, of course, will not get the answer until the deferred is triggered.
# Do everything properly from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic, irc from twisted.web import resource, server, static, xmlrpc import cgi def catchError(err): return "Internal error in server" class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): d = self.factory.getUser(user) d.addErrback(catchError) def writeValue(value): self.transport.write(value) self.transport.loseConnection() d.addCallback(writeValue) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): if len(self.lines) == 2: self.factory.setUser(*self.lines) class IRCReplyBot(irc.IRCClient): def connectionMade(self): self.nickname = self.factory.nickname irc.IRCClient.connectionMade(self) def privmsg(self, user, channel, msg): if user.lower() == channel.lower(): d = self.factory.getUser(msg) d.addErrback(catchError) d.addCallback(lambda m: "Status of %s: %s" % (user, m)) d.addCallback(lambda m: self.msg(user, m)) class UserStatusTree(resource.Resource): def __init__(self, service): resource.Resource.__init__(self): self.service = service def render(self, request): d = self.service.getUsers() def formatUsers(users): l = ["<li><a href="%s">%s</a></li> % (user, user) for user in users] return '<ul>'+''.join(l)+'</ul>' d.addCallback(formatUsers) d.addCallback(request.write) d.addCallback(lambda _: request.finish()) return server.NOT_DONE_YET def getChild(self, path, request): return UserStatus(path, self.service) class UserStatus(resource.Resource): def __init__(self, user, service): resource.Resource.__init__(self): self.user = user self.service = service def render(self, request): d = self.service.getUser(self.user) d.addCallback(cgi.escape) d.addCallback(lambda m: '<h1>%s</h1>'%self.user+'<p>%s</p>'%m) d.addCallback(request.write) d.addCallback(lambda _: request.finish()) return server.NOT_DONE_YET class UserStatusXR(xmlrpc.XMLPRC): def __init__(self, service): xmlrpc.XMLRPC.__init__(self) self.service = service def xmlrpc_getUser(self, user): return self.service.getUser(user) class FingerService(app.ApplicationService): def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getUsers(self): return defer.succeed(self.users.keys()) def getFingerFactory(self): f = protocol.ServerFactory() f.protocol = FingerProtocol f.getUser = self.getUser return f def getResource(self): r = UserStatusTree(self) x = UserStatusXR(self) r.putChild('RPC2.0', x) return r def getIRCBot(self, nickname): f = protocol.ReconnectingClientFactory() f.protocol = IRCReplyBot f.nickname = nickname f.getUser = self.getUser return f application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, f.getFingerFactory()) application.listenTCP(80, server.Site(f.getResource())) application.connectTCP('irc.freenode.org', 6667, f.getIRCBot('finger-bot'))
The last version of the application had a lot of hacks. We avoided subclassing, did not support things like user listings in the web support, and removed all blank lines -- all in the interest of code which is shorter. Here we take a step back, subclass what is more naturally a subclass, make things which should take multiple lines take them, etc. This shows a much better style of developing Twisted applications, though the hacks in the previous stages are sometimes used in throw-away prototypes.
# Do everything properly, and componentize from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic, irc from twisted.python import components from twisted.web import resource, server, static, xmlrpc import cgi class IFingerService(components.Interface): def getUser(self, user): '''Return a deferred returning a string''' def getUsers(self): '''Return a deferred returning a list of strings''' class IFingerSettingService(components.Interface): def setUser(self, user, status): '''Set the user's status to something''' def catchError(err): return "Internal error in server" class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): d = self.factory.getUser(user) d.addErrback(catchError) def writeValue(value): self.transport.write(value) self.transport.loseConnection() d.addCallback(writeValue) class IFingerFactory(components.Interface): def getUser(self, user): """Return a deferred returning a string"""" def buildProtocol(self, addr): """Return a protocol returning a string"""" class FingerFactoryFromService(protocol.ServerFactory): __implements__ = IFingerFactory, protocol = FingerProtocol def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser(user) components.registerAdapter(FingerFactoryFromService, IFingerService) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): if len(self.lines) == 2: self.factory.setUser(*self.lines) class IFingerSetterFactory(components.Interface): def setUser(self, user, status): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol returning a string""" class FingerSetterFactoryFromService(protocol.ServerFactory): __implements__ = IFingerSetterFactory, protocol = FingerSetterProtocol def __init__(self, service): self.service = service def setUser(self, user, status): self.service.setUser(user, status) components.registerAdapter(FingerSetterFactoryFromService, IFingerSettingService) class IRCReplyBot(irc.IRCClient): def connectionMade(self): self.nickname = self.factory.nickname irc.IRCClient.connectionMade(self) def privmsg(self, user, channel, msg): if user.lower() == channel.lower(): d = self.factory.getUser(msg) d.addErrback(catchError) d.addCallback(lambda m: "Status of %s: %s" % (user, m)) d.addCallback(lambda m: self.msg(user, m)) class IIRCClientFactory(components.Interface): ''' @ivar nickname ''' def getUser(self, user): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol""" class IRCClientFactoryFromService(protocol.ClientFactory): __implements__ = IIRCClientFactory, protocol = IRCReplyBot nickname = None def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser() components.registerAdapter(IRCClientFactoryFromService, IFingerService) class UserStatusTree(resource.Resource): def __init__(self, service): resource.Resource.__init__(self): self.putChild('RPC2.0', UserStatusXR(self.service)) self.service = service def render(self, request): d = self.service.getUsers() def formatUsers(users): l = ["<li><a href="%s">%s</a></li> % (user, user) for user in users] return '<ul>'+''.join(l)+'</ul>' d.addCallback(formatUsers) d.addCallback(request.write) d.addCallback(lambda _: request.finish()) return server.NOT_DONE_YET def getChild(self, path, request): return UserStatus(path, self.service) components.registerAdapter(UserStatusTree, IFingerService) class UserStatus(resource.Resource): def __init__(self, user, service): resource.Resource.__init__(self): self.user = user self.service = service def render(self, request): d = self.service.getUser(self.user) d.addCallback(cgi.escape) d.addCallback(lambda m: '<h1>%s</h1>'%self.user+'<p>%s</p>'%m) d.addCallback(request.write) d.addCallback(lambda _: request.finish()) return server.NOT_DONE_YET class UserStatusXR(xmlrpc.XMLPRC): def __init__(self, service): xmlrpc.XMLRPC.__init__(self) self.service = service def xmlrpc_getUser(self, user): return self.service.getUser(user) class FingerService(app.ApplicationService): __implements__ = IFingerService, def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getUsers(self): return defer.succeed(self.users.keys()) application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, IFingerFactory(f)) application.listenTCP(80, server.Site(resource.IResource(f))) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i)
In the last version, the service class was three times longer than any other class, and was hard to understand. This was because it turned out to have multiple responsibilities. It had to know how to access user information, by scheduling a reread of the file ever half minute, but also how to display itself in a myriad of protocols. Here, we used the component-based architecture that Twisted provides to achieve a separation of concerns. All the service is responsible for, now, is supporting getUser/getUsers. It declares its support via the __implements__ keyword. Then, adapters are used to make this service look like an appropriate class for various things: for supplying a finger factory to listenTCP, for supplying a resource to site's constructor, and to provide an IRC client factory for connectTCP. All the adapters use are the methods in FingerService they are declared to use: getUser/getUsers. We could, of course, skipped the interfaces and let the configuration code use things like FingerFactoryFromService(f) directly. However, using interfaces provides the same flexibility inheritance gives: future subclasses can override the adapters.
class MemoryFingerService(app.ApplicationService): __implements__ = IFingerService, IFingerSetterService def __init__(self, *args, **kwargs): app.ApplicationService.__init__(self, *args) self.users = kwargs def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getUsers(self): return defer.succeed(self.users.keys()) def setUser(self, user, status): self.users[user] = status application = app.Application('finger', uid=1, gid=1) # New constructor call f = MemoryFingerService(application, 'finger', moshez='Happy and well') application.listenTCP(79, IFingerFactory(f)) application.listenTCP(80, server.Site(resource.IResource(f))) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i) # New: run setter too application.listenTCP(1079, IFingerSetterFactory(f), interface='127.0.0.1')
Here we show just how convenient it is to implement new backends when we move to a component based architecture. Note that here we also use an interface we previously wrote, FingerSetterFactory, by supporting one single method. We manage to preserve the service's ignorance of the network.
class LocalFingerService(app.ApplicationService): __implements__ = IFingerService def getUser(self, user): return utils.getProcessOutput("finger", [user]) def getUsers(self): return defer.succeed([]) application = app.Application('finger', uid=1, gid=1) f = LocalFingerService(application, 'finger') application.listenTCP(79, IFingerFactory(f)) application.listenTCP(80, server.Site(resource.IResource(f))) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i)
We have already wrote this, but now we get more for less work: the network code is completely separate from the backend.
import pwd class LocalFingerService(app.ApplicationService): __implements__ = IFingerService def getUser(self, user): try: entry = pwd.getpwnam(user) except KeyError: return "No such user" try: f=file(os.path.join(entry[5],'.plan')) except (IOError, OSError): return "No such user" data = f.read() f.close() return data def getUsers(self): return defer.succeed([]) application = app.Application('finger', uid=1, gid=1) f = LocalFingerService(application, 'finger') application.listenTCP(79, IFingerFactory(f)) application.listenTCP(80, server.Site(resource.IResource(f))) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i)
Not much to say about that, except to indicate that by now we can be churning out backends like crazy. Feel like doing a backend for advogato, for example? Dig out the XML-RPC client support Twisted has, and get to work!
# Do everything properly, and componentize from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic, irc from twisted.python import components from twisted.web import resource, server, static, xmlrpc, microdom from twisted.web.woven import page, widget import cgi class IFingerService(components.Interface): def getUser(self, user): '''Return a deferred returning a string''' def getUsers(self): '''Return a deferred returning a list of strings''' class IFingerSettingService(components.Interface): def setUser(self, user, status): '''Set the user's status to something''' def catchError(err): return "Internal error in server" class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): d = self.factory.getUser(user) d.addErrback(catchError) def writeValue(value): self.transport.write(value) self.transport.loseConnection() d.addCallback(writeValue) class IFingerFactory(components.Interface): def getUser(self, user): """Return a deferred returning a string"""" def buildProtocol(self, addr): """Return a protocol returning a string"""" class FingerFactoryFromService(protocol.ServerFactory): __implements__ = IFingerFactory, protocol = FingerProtocol def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser(user) components.registerAdapter(FingerFactoryFromService, IFingerService) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): if len(self.lines) == 2: self.factory.setUser(*self.lines) class IFingerSetterFactory(components.Interface): def setUser(self, user, status): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol returning a string""" class FingerSetterFactoryFromService(protocol.ServerFactory): __implements__ = IFingerSetterFactory, protocol = FingerSetterProtocol def __init__(self, service): self.service = service def setUser(self, user, status): self.service.setUser(user, status) components.registerAdapter(FingerSetterFactoryFromService, IFingerSettingService) class IRCReplyBot(irc.IRCClient): def connectionMade(self): self.nickname = self.factory.nickname irc.IRCClient.connectionMade(self) def privmsg(self, user, channel, msg): if user.lower() == channel.lower(): d = self.factory.getUser(msg) d.addErrback(catchError) d.addCallback(lambda m: "Status of %s: %s" % (user, m)) d.addCallback(lambda m: self.msg(user, m)) class IIRCClientFactory(components.Interface): ''' @ivar nickname ''' def getUser(self, user): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol""" class IRCClientFactoryFromService(protocol.ClientFactory): __implements__ = IIRCClientFactory, protocol = IRCReplyBot nickname = None def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser() components.registerAdapter(IRCClientFactoryFromService, IFingerService) class UsersModel(model.MethodModel): def __init__(self, service): self.service = service def wmfactory_users(self): return self.service.getUsers() components.registerAdapter(UsersModel, IFingerService) class UserStatusTree(page.Page): template = """<html><head><title>Users</title><head><body> <h1>Users</h1> <ul model="users" view="List"> <li pattern="listItem" /><a view="Link" model="." href="dummy"><span model="." view="Text" /></a> </ul></body></html>""" def initialize(self, **kwargs): self.putChild('RPC2.0', UserStatusXR(self.model.service)) def getDynamicChild(self, path, request): return UserStatus(user=path, service=self.model.service) components.registerAdapter(UserStatusTree, IFingerService) class UserStatus(page.Page): template='''<html><head><title view="Text" model="user"/></heaD> <body><h1 view="Text" model="user"/> <p mode="status" view="Text" /> </body></html>''' def initialize(self, **kwargs): self.user = kwargs['user'] self.service = kwargs['service'] def wmfactory_user(self): return self.user def wmfactory_status(self): return self.service.getUser(self.user) class UserStatusXR(xmlrpc.XMLPRC): def __init__(self, service): xmlrpc.XMLRPC.__init__(self) self.service = service def xmlrpc_getUser(self, user): return self.service.getUser(user) class FingerService(app.ApplicationService): __implements__ = IFingerService, def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getUsers(self): return defer.succeed(self.users.keys()) application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, IFingerFactory(f)) application.listenTCP(80, server.Site(resource.IResource(f))) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i)
Here we convert to using Woven, instead of manually constructing HTML snippets. Woven is a sophisticated web templating system. Its main features are to disallow any code inside the HTML, and transparent integration with deferred results.
# Do everything properly, and componentize from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic, irc from twisted.python import components from twisted.web import resource, server, static, xmlrpc, microdom from twisted.web.woven import page, widget from twisted.spread import pb import cgi class IFingerService(components.Interface): def getUser(self, user): '''Return a deferred returning a string''' def getUsers(self): '''Return a deferred returning a list of strings''' class IFingerSettingService(components.Interface): def setUser(self, user, status): '''Set the user's status to something''' def catchError(err): return "Internal error in server" class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): d = self.factory.getUser(user) d.addErrback(catchError) def writeValue(value): self.transport.write(value) self.transport.loseConnection() d.addCallback(writeValue) class IFingerFactory(components.Interface): def getUser(self, user): """Return a deferred returning a string"""" def buildProtocol(self, addr): """Return a protocol returning a string"""" class FingerFactoryFromService(protocol.ServerFactory): __implements__ = IFingerFactory, protocol = FingerProtocol def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser(user) components.registerAdapter(FingerFactoryFromService, IFingerService) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): if len(self.lines) == 2: self.factory.setUser(*self.lines) class IFingerSetterFactory(components.Interface): def setUser(self, user, status): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol returning a string""" class FingerSetterFactoryFromService(protocol.ServerFactory): __implements__ = IFingerSetterFactory, protocol = FingerSetterProtocol def __init__(self, service): self.service = service def setUser(self, user, status): self.service.setUser(user, status) components.registerAdapter(FingerSetterFactoryFromService, IFingerSettingService) class IRCReplyBot(irc.IRCClient): def connectionMade(self): self.nickname = self.factory.nickname irc.IRCClient.connectionMade(self) def privmsg(self, user, channel, msg): if user.lower() == channel.lower(): d = self.factory.getUser(msg) d.addErrback(catchError) d.addCallback(lambda m: "Status of %s: %s" % (user, m)) d.addCallback(lambda m: self.msg(user, m)) class IIRCClientFactory(components.Interface): ''' @ivar nickname ''' def getUser(self, user): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol""" class IRCClientFactoryFromService(protocol.ClientFactory): __implements__ = IIRCClientFactory, protocol = IRCReplyBot nickname = None def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser() components.registerAdapter(IRCClientFactoryFromService, IFingerService) class UsersModel(model.MethodModel): def __init__(self, service): self.service = service def wmfactory_users(self): return self.service.getUsers() components.registerAdapter(UsersModel, IFingerService) class UserStatusTree(page.Page): template = """<html><head><title>Users</title><head><body> <h1>Users</h1> <ul model="users" view="List"> <li pattern="listItem" /><a view="Link" model="." href="dummy"><span model="." view="Text" /></a> </ul></body></html>""" def initialize(self, **kwargs): self.putChild('RPC2.0', UserStatusXR(self.model.service)) def getDynamicChild(self, path, request): return UserStatus(user=path, service=self.model.service) components.registerAdapter(UserStatusTree, IFingerService) class UserStatus(page.Page): template='''<html><head><<title view="Text" model="user"/></heaD> <body><h1 view="Text" model="user"/> <p mode="status" view="Text" /> </body></html>''' def initialize(self, **kwargs): self.user = kwargs['user'] self.service = kwargs['service'] def wmfactory_user(self): return self.user def wmfactory_status(self): return self.service.getUser(self.user) class UserStatusXR(xmlrpc.XMLPRC): def __init__(self, service): xmlrpc.XMLRPC.__init__(self) self.service = service def xmlrpc_getUser(self, user): return self.service.getUser(user) class IPerspectiveFinger(components.Interface): def remote_getUser(self, username): """return a user's status""" def remote_getUsers(self): """return a user's status""" class PerspectiveFingerFromService(pb.Root): __implements__ = IPerspectiveFinger, def __init__(self, service): self.service = service def remote_getUser(self, username): return self.service.getUser(username) def remote_getUsers(self): return self.service.getUsers() components.registerAdapter(PerspectiveFingerFromService, IFingerService) class FingerService(app.ApplicationService): __implements__ = IFingerService, def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getUsers(self): return defer.succeed(self.users.keys()) application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, IFingerFactory(f)) application.listenTCP(80, server.Site(resource.IResource(f))) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i) application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
We add support for perspective broker, Twisted's native remote object protocol. Now, Twisted clients will not have to go through XML-RPCish contortions to get information about users.
# Do everything properly, and componentize from twisted.internet import protocol, reactor, defer, app from twisted.protocols import basic, irc from twisted.python import components from twisted.web import resource, server, static, xmlrpc, microdom from twisted.web.woven import page, widget from twisted.spread import pb from OpenSSL import SSL import cgi class IFingerService(components.Interface): def getUser(self, user): '''Return a deferred returning a string''' def getUsers(self): '''Return a deferred returning a list of strings''' class IFingerSettingService(components.Interface): def setUser(self, user, status): '''Set the user's status to something''' def catchError(err): return "Internal error in server" class FingerProtocol(basic.LineReceiver): def lineReceived(self, user): d = self.factory.getUser(user) d.addErrback(catchError) def writeValue(value): self.transport.write(value) self.transport.loseConnection() d.addCallback(writeValue) class IFingerFactory(components.Interface): def getUser(self, user): """Return a deferred returning a string"""" def buildProtocol(self, addr): """Return a protocol returning a string"""" class FingerFactoryFromService(protocol.ServerFactory): __implements__ = IFingerFactory, protocol = FingerProtocol def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser(user) components.registerAdapter(FingerFactoryFromService, IFingerService) class FingerSetterProtocol(basic.LineReceiver): def connectionMade(self): self.lines = [] def lineReceived(self, line): self.lines.append(line) def connectionLost(self): if len(self.lines) == 2: self.factory.setUser(*self.lines) class IFingerSetterFactory(components.Interface): def setUser(self, user, status): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol returning a string""" class FingerSetterFactoryFromService(protocol.ServerFactory): __implements__ = IFingerSetterFactory, protocol = FingerSetterProtocol def __init__(self, service): self.service = service def setUser(self, user, status): self.service.setUser(user, status) components.registerAdapter(FingerSetterFactoryFromService, IFingerSettingService) class IRCReplyBot(irc.IRCClient): def connectionMade(self): self.nickname = self.factory.nickname irc.IRCClient.connectionMade(self) def privmsg(self, user, channel, msg): if user.lower() == channel.lower(): d = self.factory.getUser(msg) d.addErrback(catchError) d.addCallback(lambda m: "Status of %s: %s" % (user, m)) d.addCallback(lambda m: self.msg(user, m)) class IIRCClientFactory(components.Interface): ''' @ivar nickname ''' def getUser(self, user): """Return a deferred returning a string""" def buildProtocol(self, addr): """Return a protocol""" class IRCClientFactoryFromService(protocol.ClientFactory): __implements__ = IIRCClientFactory, protocol = IRCReplyBot nickname = None def __init__(self, service): self.service = service def getUser(self, user): return self.service.getUser() components.registerAdapter(IRCClientFactoryFromService, IFingerService) class UsersModel(model.MethodModel): def __init__(self, service): self.service = service def wmfactory_users(self): return self.service.getUsers() components.registerAdapter(UsersModel, IFingerService) class UserStatusTree(page.Page): template = """<html><head><title>Users</title><head><body> <h1>Users</h1> <ul model="users" view="List"> <li pattern="listItem" /><a view="Link" model="." href="dummy"><span model="." view="Text" /></a> </ul></body></html>""" def initialize(self, **kwargs): self.putChild('RPC2.0', UserStatusXR(self.model.service)) def getDynamicChild(self, path, request): return UserStatus(user=path, service=self.model.service) components.registerAdapter(UserStatusTree, IFingerService) class UserStatus(page.Page): template='''<html><head><title view="Text" model="user"/></heaD> <body><h1 view="Text" model="user"/> <p mode="status" view="Text" /> </body></html>''' def initialize(self, **kwargs): self.user = kwargs['user'] self.service = kwargs['service'] def wmfactory_user(self): return self.user def wmfactory_status(self): return self.service.getUser(self.user) class UserStatusXR(xmlrpc.XMLPRC): def __init__(self, service): xmlrpc.XMLRPC.__init__(self) self.service = service def xmlrpc_getUser(self, user): return self.service.getUser(user) class IPerspectiveFinger(components.Interface): def remote_getUser(self, username): """return a user's status""" def remote_getUsers(self): """return a user's status""" class PerspectiveFingerFromService(pb.Root): __implements__ = IPerspectiveFinger, def __init__(self, service): self.service = service def remote_getUser(self, username): return self.service.getUser(username) def remote_getUsers(self): return self.service.getUsers() components.registerAdapter(PerspectiveFingerFromService, IFingerService) class FingerService(app.ApplicationService): __implements__ = IFingerService, def __init__(self, file, *args, **kwargs): app.ApplicationService.__init__(self, *args, **kwargs) self.file = file def startService(self): app.ApplicationService.startService(self) self._read() def _read(self): self.users = {} for line in file(self.file): user, status = line.split(':', 1) self.users[user] = status self.call = reactor.callLater(30, self._read) def stopService(self): app.ApplicationService.stopService(self) self.call.cancel() def getUser(self, user): return defer.succeed(self.users.get(u, "No such user")) def getUsers(self): return defer.succeed(self.users.keys()) class ServerContextFactory: def getContext(self): """Create an SSL context. This is a sample implementation that loads a certificate from a file called 'server.pem'.""" ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_certificate_file('server.pem') ctx.use_privatekey_file('server.pem') return ctx application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, IFingerFactory(f)) site = server.Site(resource.IResource(f)) application.listenTCP(80, site) application.listenSSL(443, site, ServerContextFactory()) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i) application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
All we need to do to code an HTTPS site is just write a context factory (in this case, which loads the certificate from a certain file) and then use the listenSSL method. Note that one factory (in this case, a site) can listen on multiple ports with multiple protocols.
class FingerClient(protocol.Protocol): def connectionMade(self): self.transport.write(self.factory.user+"\r\n") self.buf = [] def dataReceived(self, data): self.buf.append(data) def connectionLost(self): self.factory.gotData(''.join(self.buf)) class FingerClientFactory(protocol.ClientFactory): protocol = FingerClient def __init__(self, user): self.user = user self.d = defer.Deferred() def clientConnectionFailed(self, _, reason): self.d.errback(reason) def gotData(self, data): self.d.callback(data) def finger(user, host, port=79): f = FingerClientFactory(user) reactor.connectTCP(host, port, f) return f.d class ProxyFingerService(app.ApplicationService): __implements__ = IFingerService def getUser(self, user): user, host = user.split('@', 1) ret = finger(user, host) ret.addErrback(lambda _: "Could not connect to remote host") return ret def getUsers(self): return defer.succeed([]) application = app.Application('finger', uid=1, gid=1) f = ProxyFingerService(application, 'finger') application.listenTCP(79, IFingerFactory(f))
Writing new clients with Twisted is much like writing new servers. We implement the protocol, which just gathers up all the data, and give it to the factory. The factory keeps a deferred which is triggered if the connection either fails or succeeds. When we use the client, we first make sure the deferred will never fail, by producing a message in that case. Implementing a wrapper around client which just returns the deferred is a common pattern. While being less flexible than using the factory directly, it is also more convenient.
application=
line.from twisted.internet import app from finger import FingerService, IIRCclient, ServerContextFactory, \ IFingerFactory, IPerspectiveFinger from twisted.web import resource, server from twisted.spread import pb application = app.Application('finger', uid=1, gid=1) f = FingerService('/etc/users', application, 'finger') application.listenTCP(79, IFingerFactory(f)) r = resource.IResource(f) r.templateDirectory = '/usr/share/finger/templates/' site = server.Site(r) application.listenTCP(80, site) application.listenSSL(443, site, ServerContextFactory()) i = IIRCClientFactory(f) i.nickname = 'fingerbot' application.connectTCP('irc.freenode.org', 6667, i) application.listenTCP(8889, pb.BrokerFactory(IPerspectiveFinger(f))
We can also supply easy configuration for common cases
# in finger.py moudle def updateApplication(app, **kwargs): f = FingerService(kwargs['users'], application, 'finger') application.listenTCP(79, IFingerFactory(f)) r = resource.IResource(f) r.templateDirectory = kwargs['templates'] site = server.Site(r) app.listenTCP(80, site) if kwargs.get('ssl'): app.listenSSL(443, site, ServerContextFactory()) if kwargs.has_key('ircnick'): i = IIRCClientFactory(f) i.nickname = kwargs['ircnick'] ircServer = kwargs['ircserver'] application.connectTCP(ircserver, 6667, i) if kwargs.has_key('pbport'): application.listenTCP(int(kwargs['pbport']), pb.BrokerFactory(IPerspectiveFinger(f))
And we can write simpler files now:
# simple-finger.tpy from twisted.internet import app import finger application = app.Application('finger', uid=1, gid=1) finger.updateApplication(application, users='/etc/users', templatesDirectory='/usr/share/finger/templates', ssl=1, ircnick='fingerbot', ircserver='irc.freenode.net', pbport=8889 )
Note: the finger user still has ultimate power: he can use updateApplication, or he can use the lower-level interface if he has specific needs (maybe an ircserver on some other port? maybe we want the non-ssl webserver to listen only locally? etc. etc.) This is an important design principle: never force a layer of abstraction: allow usage of layers of abstractions.
The pasta theory of design:
So far, the user had to be somewhat of a programmer to use this. Maybe we can eliminate even that? Move old code to "finger/service.py", put empty "__init__.py" and...
# finger/tap.py from twisted.python import usage from finger import service class Options(usage.Options): optParams = [ ['users', 'u', '/etc/users'], ['templatesDirectory', 't', '/usr/share/finger/templates'], ['ircnick', 'n', 'fingerbot'], ['ircserver', None, 'irc.freenode.net'], ['pbport', 'p', 8889], ] optFlags = [['ssl', 's']] def updateApplication(app, config): service.updateApplication(app, **config)
And register it all:
#finger/plugins.tml register('Finger', 'finger.tap', type='tap', tapname='finger')
And now, the following works
% mktap finger --users=/usr/local/etc/users --ircnick=moshez-finger % sudo twistd -f finger.tap
If we already have the "finger" package installed, we can achieve easy integration:
on Debian--
% tap2deb --unsigned -m "Foo" --type=python finger.tpy % sudo dpkg -i .build/*.deb
On Red Hat [or Mandrake]
% tap2rpm --type=python finger.tpy #[maybe other options needed] % sudo rpm -i .build/*.rpm
Will properly register configuration files, init.d sripts, etc. etc.
If it doesn't work on your favourite OS: patches accepted!