I have an IRC bot that I wrote using the Twisted Python IRC protocols. I want to be able to run commands while still allowing the bot to listen and execute other commands simultaneously.
For example, let's say I have command that will print a large text file to a channel. If I wanted to stop the command while it was running by entering "!stop" into the channel, how could I accomplish this? Or let's say I want to do "!print largefile" in one channel and then go to a different channel and type "!print anotherfile" and have it print that file to the other channel before even finishing printing the first file.
I'm thinking that I would use threading for this purpose? I'm not quite sure.
EDIT (to clarify):
def privmsg(self, user, channel, msg):
nick = user.split('!', 1)[0]
parts = msg.split(' ')
trigger = parts[0]
data = parts[1:]
if trigger == '!printfile':
myfile = open('/files/%s' % data[0], 'r')
for line in myfile:
line = line.strip('/r/n')
self.msg(channel, line)
if trigger == '!stop':
CODE TO STOP THE CURRENTLY RUNNING PRINTFILE COMMAND
If I wanted to run !printfile
in two channels at once or stop the printfile command while it is running, what should I do?
The reason you can't interrupt your printfile command is that it contains a loop over the entire contents of a file. That means the privmsg
function will run until it has read and sent all lines from the file. Only after it finishes that work will it return.
Twisted is a single-threaded cooperative multitasking system. Only one part of your program can run at a time. Before the next line of input from the irc server can be handled by your irc bot, privmsg
has to return.
However, Twisted is also good at dealing with events and managing concurrency. So, one solution to this problem is to send the file using one of the tools included in Twisted (instead of a for loop) - a tool that cooperates with the rest of the system and allows other events to be handled in the meanwhile.
Here's a brief example (untested, and with some obvious problems (such as the poor behavior when two printfile commands arrive too close together) that I won't try to fix here):
from twisted.internet.task import cooperate
....
def privmsg(self, user, channel, msg):
nick = user.split('!', 1)[0]
parts = msg.split(' ')
trigger = parts[0]
data = parts[1:]
if trigger == '!printfile':
self._printfile(channel, data[0])
if trigger == '!stop':
self._stopprintfile()
def _printfile(self, channel, name):
myfile = open('/files/%s' % (name,), 'r')
self._printfiletask = cooperate(
self.msg(channel, line.rstrip('\r\n')) for line in myfile)
def _stopprintfile(self):
self._printfiletask.stop()
This uses twisted.internet.task.cooperate
, a helper function which accepts an iterator (including generators) and runs them in a way that cooperates with the rest of your application. It does this by iterating the iterator a few times, then letting other work run, then coming back to the iterator, and so on until the iterator is exhausted.
This means new messages from irc will be processed even while the file is being sent.
However, another point to consider is that irc servers typically include flood protection, which means sending many lines to them very quickly may get your bot disconnected. Even in the best case, the irc server may buffer the lines and only release them to the network at large slowly. If the bot has already sent the lines and they are sitting in the irc server's buffer, you won't be able to stop them from appearing on the network by telling the bot to stop (since it finished already). And further, because of this, Twisted's irc client also has buffering, so even after you call self.msg
, the line may not actually be sent, because the irc client is buffering lines in order to avoid sending them so fast that the irc server kicks the bot off the network. Since the code I wrote only deals with calls to self.msg
, you may still not actually be able to stop lines from being sent, if they have all already entered the irc client's local buffer.
One obvious (perhaps not ideal) solution to all of those issues is to slightly complicate the iterator used in _printfile
, by inserting a new delay there:
from twisted.internet import reactor
from twisted.internet.task import deferLater
def _printfileiterator(self, channel, myfile):
for line in myfile:
self.msg(channel, line)
yield deferLater(reactor, 2, lambda: None)
def _printfile(self, channel, name):
myfile = open('/files/%s' % (name,), 'r')
self._printfiletask = cooperate(self._printfileiterator(channel, myfile))
Here, I've changed the iterator so that the elements that come out of it are Deferreds from deferLater
(previously, the elements were all None
, since that is the return value of self.msg
).
When cooperate
encounters a Deferred
, it stops working on that iterator until after that Deferred
fires. deferLater
used in this way is basically a cooperative sleep function. It returns a Deferred
that won't fire until 2 seconds have passed (and then it fires with None
, which cooperate
doesn't particularly care about). After it fires, cooperate
will resume working on the iterator though. So now _printfile
only sends one line every two seconds, something that will be much easier to interrupt with a stop command.