[Buildbot-commits] buildbot/buildbot/status words.py,1.48,1.49
Brian Warner
warner at users.sourceforge.net
Sun Jun 17 21:10:30 UTC 2007
Update of /cvsroot/buildbot/buildbot/buildbot/status
In directory sc8-pr-cvs3.sourceforge.net:/tmp/cvs-serv21025/buildbot/status
Modified Files:
words.py
Log Message:
[project @ refactor the irc bot, in preparation for other IM status backends]
Original author: warner at lothar.com
Date: 2007-05-17 22:41:10+00:00
Index: words.py
===================================================================
RCS file: /cvsroot/buildbot/buildbot/buildbot/status/words.py,v
retrieving revision 1.48
retrieving revision 1.49
diff -u -d -r1.48 -r1.49
--- words.py 7 Feb 2007 04:25:29 -0000 1.48
+++ words.py 17 Jun 2007 21:10:27 -0000 1.49
@@ -5,6 +5,7 @@
import re, shlex
+from zope.interface import Interface, implements
from twisted.internet import protocol, reactor
from twisted.words.protocols import irc
from twisted.python import log, failure
@@ -26,17 +27,15 @@
hasStarted = False
timer = None
- def __init__(self, parent, reply):
+ def __init__(self, parent):
self.parent = parent
- self.reply = reply
self.timer = reactor.callLater(5, self.soon)
def soon(self):
del self.timer
if not self.hasStarted:
- self.parent.reply(self.reply,
- "The build has been queued, I'll give a shout"
- " when it starts")
+ self.parent.send("The build has been queued, I'll give a shout"
+ " when it starts")
def started(self, c):
self.hasStarted = True
@@ -48,14 +47,28 @@
response = "build #%d forced" % s.getNumber()
if eta is not None:
response = "build forced [ETA %s]" % self.parent.convertTime(eta)
- self.parent.reply(self.reply, response)
- self.parent.reply(self.reply,
- "I'll give a shout when the build finishes")
+ self.parent.send(response)
+ self.parent.send("I'll give a shout when the build finishes")
d = s.waitUntilFinished()
- d.addCallback(self.parent.buildFinished, self.reply)
+ d.addCallback(self.parent.buildFinished)
-class IrcStatusBot(irc.IRCClient):
+class Contact:
+ """I hold the state for a single user's interaction with the buildbot.
+
+ This base class provides all the basic behavior (the queries and
+ responses). Subclasses for each channel type (IRC, different IM
+ protocols) are expected to provide the lower-level send/receive methods.
+
+ There will be one instance of me for each user who interacts personally
+ with the buildbot. There will be an additional instance for each
+ 'broadcast contact' (chat rooms, IRC channels as a whole).
+ """
+
+ def __init__(self, channel, username):
+ self.channel = channel
+ self.username = username
+
silly = {
"What happen ?": "Somebody set up us the bomb.",
"It's You !!": ["How are you gentlemen !!",
@@ -64,91 +77,6 @@
"What you say !!": ["You have no chance to survive make your time.",
"HA HA HA HA ...."],
}
- def __init__(self, nickname, password, channels, status, categories):
- """
- @type nickname: string
- @param nickname: the nickname by which this bot should be known
- @type password: string
- @param password: the password to use for identifying with Nickserv
- @type channels: list of strings
- @param channels: the bot will maintain a presence in these channels
- @type status: L{buildbot.status.builder.Status}
- @param status: the build master's Status object, through which the
- bot retrieves all status information
- """
- self.nickname = nickname
- self.channels = channels
- self.password = password
- self.status = status
- self.categories = categories
- self.counter = 0
- self.hasQuit = 0
-
- def signedOn(self):
- if self.password:
- self.msg("Nickserv", "IDENTIFY " + self.password)
- for c in self.channels:
- self.join(c)
- def joined(self, channel):
- log.msg("I have joined", channel)
- def left(self, channel):
- log.msg("I have left", channel)
- def kickedFrom(self, channel, kicker, message):
- log.msg("I have been kicked from %s by %s: %s" % (channel,
- kicker,
- message))
-
- # input
- def privmsg(self, user, channel, message):
- user = user.split('!', 1)[0] # rest is ~user at hostname
- # channel is '#twisted' or 'buildbot' (for private messages)
- channel = channel.lower()
- #print "privmsg:", user, channel, message
- if channel == self.nickname:
- # private message
- message = "%s: %s" % (self.nickname, message)
- reply = user
- else:
- reply = channel
- if message.startswith("%s:" % self.nickname):
- message = message[len("%s:" % self.nickname):]
-
- message = message.lstrip()
- if self.silly.has_key(message):
- return self.doSilly(user, reply, message)
-
- parts = message.split(' ', 1)
- if len(parts) == 1:
- parts = parts + ['']
- cmd, args = parts
- log.msg("irc command", cmd)
-
- meth = self.getCommandMethod(cmd)
- if not meth and message[-1] == '!':
- meth = self.command_EXCITED
-
- error = None
- try:
- if meth:
- meth(user, reply, args.strip())
- except UsageError, e:
- self.reply(reply, str(e))
- except:
- f = failure.Failure()
- log.err(f)
- error = "Something bad happened (see logs): %s" % f.type
-
- if error:
- try:
- self.reply(reply, error)
- except:
- log.err()
-
- #self.say(channel, "count %d" % self.counter)
- self.counter += 1
- def reply(self, dest, message):
- # maybe self.notice(dest, message) instead?
- self.msg(dest, message)
def getCommandMethod(self, command):
meth = getattr(self, 'command_' + command.upper(), None)
@@ -156,16 +84,16 @@
def getBuilder(self, which):
try:
- b = self.status.getBuilder(which)
+ b = self.channel.status.getBuilder(which)
except KeyError:
raise UsageError, "no such builder '%s'" % which
return b
def getControl(self, which):
- if not self.control:
+ if not self.channel.control:
raise UsageError("builder control is not enabled")
try:
- bc = self.control.getBuilder(which)
+ bc = self.channel.control.getBuilder(which)
except KeyError:
raise UsageError("no such builder '%s'" % which)
return bc
@@ -174,9 +102,9 @@
"""
@rtype: list of L{buildbot.process.builder.Builder}
"""
- names = self.status.getBuilderNames(categories=self.categories)
+ names = self.channel.status.getBuilderNames(categories=self.categories)
names.sort()
- builders = [self.status.getBuilder(n) for n in names]
+ builders = [self.channel.status.getBuilder(n) for n in names]
return builders
def convertTime(self, seconds):
@@ -190,22 +118,22 @@
minutes = minutes - 60*hours
return "%dh%02dm%02ds" % (hours, minutes, seconds)
- def doSilly(self, user, reply, message):
+ def doSilly(self, message):
response = self.silly[message]
if type(response) != type([]):
response = [response]
when = 0.5
for r in response:
- reactor.callLater(when, self.reply, reply, r)
+ reactor.callLater(when, self.send, r)
when += 2.5
- def command_HELLO(self, user, reply, args):
- self.reply(reply, "yes?")
+ def command_HELLO(self, args):
+ self.send("yes?")
- def command_VERSION(self, user, reply, args):
- self.reply(reply, "buildbot-%s at your service" % version)
+ def command_VERSION(self, args):
+ self.send("buildbot-%s at your service" % version)
- def command_LIST(self, user, reply, args):
+ def command_LIST(self, args):
args = args.split()
if len(args) == 0:
raise UsageError, "try 'list builders'"
@@ -219,11 +147,11 @@
str += "[offline]"
str += " "
str.rstrip()
- self.reply(reply, str)
+ self.send(str)
return
command_LIST.usage = "list builders - List configured builders"
- def command_STATUS(self, user, reply, args):
+ def command_STATUS(self, args):
args = args.split()
if len(args) == 0:
which = "all"
@@ -234,12 +162,12 @@
if which == "all":
builders = self.getAllBuilders()
for b in builders:
- self.emit_status(reply, b.name)
+ self.emit_status(b.name)
return
- self.emit_status(reply, which)
+ self.emit_status(which)
command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)"
- def command_WATCH(self, user, reply, args):
+ def command_WATCH(self, args):
args = args.split()
if len(args) != 1:
raise UsageError("try 'watch <builder>'")
@@ -247,22 +175,22 @@
b = self.getBuilder(which)
builds = b.getCurrentBuilds()
if not builds:
- self.reply(reply, "there are no builds currently running")
+ self.send("there are no builds currently running")
return
for build in builds:
assert not build.isFinished()
d = build.waitUntilFinished()
- d.addCallback(self.buildFinished, reply)
+ d.addCallback(self.buildFinished)
r = "watching build %s #%d until it finishes" \
% (which, build.getNumber())
eta = build.getETA()
if eta is not None:
r += " [%s]" % self.convertTime(eta)
r += ".."
- self.reply(reply, r)
+ self.send(r)
command_WATCH.usage = "watch <which> - announce the completion of an active build"
- def buildFinished(self, b, reply):
+ def buildFinished(self, b):
results = {SUCCESS: "Success",
WARNINGS: "Warnings",
FAILURE: "Failure",
@@ -282,12 +210,12 @@
b.getNumber(),
results.get(b.getResults(), "??"))
r += " [%s]" % " ".join(b.getText())
- self.reply(reply, r)
- buildurl = self.status.getURLForThing(b)
+ self.send(r)
+ buildurl = self.channel.status.getURLForThing(b)
if buildurl:
- self.reply(reply, "Build details are at %s" % buildurl)
+ self.send("Build details are at %s" % buildurl)
- def command_FORCE(self, user, reply, args):
+ def command_FORCE(self, args):
args = shlex.split(args) # TODO: this requires python2.3 or newer
if args.pop(0) != "build":
raise UsageError("try 'force build WHICH <REASON>'")
@@ -303,11 +231,11 @@
# centralize this somewhere.
if branch and not re.match(r'^[\w\.\-\/]*$', branch):
log.msg("bad branch '%s'" % branch)
- self.reply(reply, "sorry, bad branch '%s'" % branch)
+ self.send("sorry, bad branch '%s'" % branch)
return
if revision and not re.match(r'^[\w\.\-\/]*$', revision):
log.msg("bad revision '%s'" % revision)
- self.reply(reply, "sorry, bad revision '%s'" % revision)
+ self.send("sorry, bad revision '%s'" % revision)
return
bc = self.getControl(which)
@@ -315,9 +243,8 @@
who = None # TODO: if we can authenticate that a particular User
# asked for this, use User Name instead of None so they'll
# be informed of the results.
- # TODO: or, monitor this build and announce the results through the
- # 'reply' argument.
- r = "forced: by IRC user <%s>: %s" % (user, reason)
+ # TODO: or, monitor this build and announce the results
+ r = "forced: by IRC user <%s>: %s" % (self.username, reason)
# TODO: maybe give certain users the ability to request builds of
# certain branches
s = SourceStamp(branch=branch, revision=revision)
@@ -325,16 +252,15 @@
try:
bc.requestBuildSoon(req)
except interfaces.NoSlaveError:
- self.reply(reply,
- "sorry, I can't force a build: all slaves are offline")
+ self.send("sorry, I can't force a build: all slaves are offline")
return
- ireq = IrcBuildRequest(self, reply)
+ ireq = IrcBuildRequest(self)
req.subscribe(ireq.started)
command_FORCE.usage = "force build <which> <reason> - Force a build"
- def command_STOP(self, user, reply, args):
+ def command_STOP(self, args):
args = args.split(None, 2)
if len(args) < 3 or args[0] != 'build':
raise UsageError, "try 'stop build WHICH <REASON>'"
@@ -344,13 +270,13 @@
buildercontrol = self.getControl(which)
who = None
- r = "stopped: by IRC user <%s>: %s" % (user, reason)
+ r = "stopped: by IRC user <%s>: %s" % (self.username, reason)
# find an in-progress build
builderstatus = self.getBuilder(which)
builds = builderstatus.getCurrentBuilds()
if not builds:
- self.reply(reply, "sorry, no build is currently running")
+ self.send("sorry, no build is currently running")
return
for build in builds:
num = build.getNumber()
@@ -361,11 +287,11 @@
# make it stop
buildcontrol.stopBuild(r)
- self.reply(reply, "build %d interrupted" % num)
+ self.send("build %d interrupted" % num)
command_STOP.usage = "stop build <which> <reason> - Stop a running build"
- def emit_status(self, reply, which):
+ def emit_status(self, which):
b = self.getBuilder(which)
str = "%s: " % which
state, builds = b.getState()
@@ -386,9 +312,9 @@
s += " [ETA %s]" % self.convertTime(ETA)
t.append(s)
str += ", ".join(t)
- self.reply(reply, str)
+ self.send(str)
- def emit_last(self, reply, which):
+ def emit_last(self, which):
last = self.getBuilder(which).getLastFinishedBuild()
if not last:
str = "(no builds run since last restart)"
@@ -396,9 +322,9 @@
start,finish = last.getTimes()
str = "%s secs ago: " % (int(util.now() - finish))
str += " ".join(last.getText())
- self.reply(reply, "last build [%s]: %s" % (which, str))
+ self.send("last build [%s]: %s" % (which, str))
- def command_LAST(self, user, reply, args):
+ def command_LAST(self, args):
args = args.split()
if len(args) == 0:
which = "all"
@@ -409,9 +335,9 @@
if which == "all":
builders = self.getAllBuilders()
for b in builders:
- self.emit_last(reply, b.name)
+ self.emit_last(b.name)
return
- self.emit_last(reply, which)
+ self.emit_last(which)
command_LAST.usage = "last <which> - list last build status for builder <which>"
def build_commands(self):
@@ -422,10 +348,10 @@
commands.sort()
return commands
- def command_HELP(self, user, reply, args):
+ def command_HELP(self, args):
args = args.split()
if len(args) == 0:
- self.reply(reply, "Get help on what? (try 'help <foo>', or 'commands' for a command list)")
+ self.send("Get help on what? (try 'help <foo>', or 'commands' for a command list)")
return
command = args[0]
meth = self.getCommandMethod(command)
@@ -433,56 +359,208 @@
raise UsageError, "no such command '%s'" % command
usage = getattr(meth, 'usage', None)
if usage:
- self.reply(reply, "Usage: %s" % usage)
+ self.send("Usage: %s" % usage)
else:
- self.reply(reply, "No usage info for '%s'" % command)
+ self.send("No usage info for '%s'" % command)
command_HELP.usage = "help <command> - Give help for <command>"
- def command_SOURCE(self, user, reply, args):
+ def command_SOURCE(self, args):
banner = "My source can be found at http://buildbot.sourceforge.net/"
- self.reply(reply, banner)
+ self.send(banner)
- def command_COMMANDS(self, user, reply, args):
+ def command_COMMANDS(self, args):
commands = self.build_commands()
str = "buildbot commands: " + ", ".join(commands)
- self.reply(reply, str)
+ self.send(str)
command_COMMANDS.usage = "commands - List available commands"
- def command_DESTROY(self, user, reply, args):
- self.me(reply, "readies phasers")
+ def command_DESTROY(self, args):
+ self.act("readies phasers")
- def command_DANCE(self, user, reply, args):
- reactor.callLater(1.0, self.reply, reply, "0-<")
- reactor.callLater(3.0, self.reply, reply, "0-/")
- reactor.callLater(3.5, self.reply, reply, "0-\\")
+ def command_DANCE(self, args):
+ reactor.callLater(1.0, self.send, "0-<")
+ reactor.callLater(3.0, self.send, "0-/")
+ reactor.callLater(3.5, self.send, "0-\\")
- def command_EXCITED(self, user, reply, args):
+ def command_EXCITED(self, args):
# like 'buildbot: destroy the sun!'
- self.reply(reply, "What you say!")
+ self.send("What you say!")
+
+ def handleAction(self, data, user):
+ # this is sent when somebody performs an action that mentions the
+ # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of
+ # the person who performed the action, so if their action provokes a
+ # response, they can be named.
+ if not data.endswith("s buildbot"):
+ return
+ words = data.split()
+ verb = words[-2]
+ timeout = 4
+ if verb == "kicks":
+ response = "%s back" % verb
+ timeout = 1
+ else:
+ response = "%s %s too" % (verb, user)
+ reactor.callLater(timeout, self.act, response)
+
+class IRCContact(Contact):
+ # this is the IRC-specific subclass of Contact
+
+ def __init__(self, channel, dest):
+ Contact.__init__(self, channel)
+ self.dest = dest
+
+ # userJoined(self, user, channel)
+
+ def send(self, message):
+ self.channel.msg(self.dest, message)
+ def act(self, action):
+ self.channel.me(self.dest, action)
+
+
+ def handleMessage(self, message):
+ message = message.lstrip()
+ if self.silly.has_key(message):
+ return self.doSilly(message)
+
+ parts = message.split(' ', 1)
+ if len(parts) == 1:
+ parts = parts + ['']
+ cmd, args = parts
+ log.msg("irc command", cmd)
+
+ meth = self.getCommandMethod(cmd)
+ if not meth and message[-1] == '!':
+ meth = self.command_EXCITED
+
+ error = None
+ try:
+ if meth:
+ meth(args.strip())
+ except UsageError, e:
+ self.send(str(e))
+ except:
+ f = failure.Failure()
+ log.err(f)
+ error = "Something bad happened (see logs): %s" % f.type
+
+ if error:
+ try:
+ self.send(error)
+ except:
+ log.err()
+
+ #self.say(channel, "count %d" % self.counter)
+ self.channel.counter += 1
+
+class IChannel(Interface):
+ """I represent the buildbot's presence in a particular IM scheme.
+
+ This provides the connection to the IRC server, or represents the
+ buildbot's account with an IM service. Each Channel will have zero or
+ more Contacts associated with it.
+ """
+
+class IrcStatusBot(irc.IRCClient):
+ """I represent the buildbot to an IRC server.
+ """
+ implements(IChannel)
+
+ def __init__(self, nickname, password, channels, status, categories):
+ """
+ @type nickname: string
+ @param nickname: the nickname by which this bot should be known
+ @type password: string
+ @param password: the password to use for identifying with Nickserv
+ @type channels: list of strings
+ @param channels: the bot will maintain a presence in these channels
+ @type status: L{buildbot.status.builder.Status}
+ @param status: the build master's Status object, through which the
+ bot retrieves all status information
+ """
+ self.nickname = nickname
+ self.channels = channels
+ self.password = password
+ self.status = status
+ self.categories = categories
+ self.counter = 0
+ self.hasQuit = 0
+ self.contacts = {}
+
+ def addContact(self, name, contact):
+ self.contacts[name] = contact
+
+ def getContact(self, name):
+ if name in self.contacts:
+ return self.contacts[name]
+ new_contact = IRCContact(self, name)
+ self.contacts[name] = new_contact
+ return new_contact
+
+ def deleteContact(self, contact):
+ name = contact.getName()
+ if name in self.contacts:
+ assert self.contacts[name] == contact
+ del self.contacts[name]
+
+ def log(self, msg):
+ log.msg("%s: %s" % (self, msg))
+
+
+ # the following irc.IRCClient methods are called when we have input
+
+ def privmsg(self, user, channel, message):
+ user = user.split('!', 1)[0] # rest is ~user at hostname
+ # channel is '#twisted' or 'buildbot' (for private messages)
+ channel = channel.lower()
+ #print "privmsg:", user, channel, message
+ if channel == self.nickname:
+ # private message
+ contact = self.getContact(user)
+ contact.handleMessage(message)
+ return
+ # else it's a broadcast message, maybe for us, maybe not. 'channel'
+ # is '#twisted' or the like.
+ contact = self.getContact(channel)
+ if message.startswith("%s:" % self.nickname):
+ message = message[len("%s:" % self.nickname):]
+ contact.handleMessage(message)
+ # to track users comings and goings, add code here
def action(self, user, channel, data):
#log.msg("action: %s,%s,%s" % (user, channel, data))
user = user.split('!', 1)[0] # rest is ~user at hostname
- # somebody did an action (/me actions)
- if data.endswith("s buildbot"):
- words = data.split()
- verb = words[-2]
- timeout = 4
- if verb == "kicks":
- response = "%s back" % verb
- timeout = 1
- else:
- response = "%s %s too" % (verb, user)
- reactor.callLater(timeout, self.me, channel, response)
- # userJoined(self, user, channel)
-
- # output
+ # somebody did an action (/me actions) in the broadcast channel
+ contact = self.getContact(channel)
+ if "buildbot" in data:
+ contact.handleAction(data, user)
+
+
+
+ def signedOn(self):
+ if self.password:
+ self.msg("Nickserv", "IDENTIFY " + self.password)
+ for c in self.channels:
+ self.join(c)
+
+ def joined(self, channel):
+ self.log("I have joined %s" % (channel,))
+ def left(self, channel):
+ self.log("I have left %s" % (channel,))
+ def kickedFrom(self, channel, kicker, message):
+ self.log("I have been kicked from %s by %s: %s" % (channel,
+ kicker,
+ message))
+
+ # we can using the following irc.IRCClient methods to send output. Most
+ # of these are used by the IRCContact class.
+ #
# self.say(channel, message) # broadcast
# self.msg(user, message) # unicast
# self.me(channel, action) # send action
# self.away(message='')
# self.quit(message='')
-
+
class ThrottledClientFactory(protocol.ClientFactory):
lostDelay = 2
failedDelay = 60
@@ -586,20 +664,6 @@
return base.StatusReceiverMultiService.stopService(self)
-def main():
- from twisted.internet import app
- a = app.Application("irctest")
- f = IrcStatusFactory()
- host = "localhost"
- port = 6667
- f.addNetwork((host, port), ["private", "other"])
- a.connectTCP(host, port, f)
- a.run(save=0)
-
-
-if __name__ == '__main__':
- main()
-
## buildbot: list builders
# buildbot: watch quick
# print notification when current build in 'quick' finishes
More information about the Commits
mailing list