Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/status/words.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/status/words.py')
-rw-r--r--buildbot/buildbot/status/words.py875
1 files changed, 875 insertions, 0 deletions
diff --git a/buildbot/buildbot/status/words.py b/buildbot/buildbot/status/words.py
new file mode 100644
index 0000000..0e98651
--- /dev/null
+++ b/buildbot/buildbot/status/words.py
@@ -0,0 +1,875 @@
+
+# code to deliver build status through twisted.words (instant messaging
+# protocols: irc, etc)
+
+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
+from twisted.application import internet
+
+from buildbot import interfaces, util
+from buildbot import version
+from buildbot.sourcestamp import SourceStamp
+from buildbot.process.base import BuildRequest
+from buildbot.status import base
+from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, EXCEPTION
+from buildbot.scripts.runner import ForceOptions
+
+from string import join, capitalize, lower
+
+class UsageError(ValueError):
+ def __init__(self, string = "Invalid usage", *more):
+ ValueError.__init__(self, string, *more)
+
+class IrcBuildRequest:
+ hasStarted = False
+ timer = None
+
+ def __init__(self, parent):
+ self.parent = parent
+ self.timer = reactor.callLater(5, self.soon)
+
+ def soon(self):
+ del self.timer
+ if not self.hasStarted:
+ self.parent.send("The build has been queued, I'll give a shout"
+ " when it starts")
+
+ def started(self, c):
+ self.hasStarted = True
+ if self.timer:
+ self.timer.cancel()
+ del self.timer
+ s = c.getStatus()
+ eta = s.getETA()
+ response = "build #%d forced" % s.getNumber()
+ if eta is not None:
+ response = "build forced [ETA %s]" % self.parent.convertTime(eta)
+ self.parent.send(response)
+ self.parent.send("I'll give a shout when the build finishes")
+ d = s.waitUntilFinished()
+ d.addCallback(self.parent.watchedBuildFinished)
+
+
+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):
+ self.channel = channel
+ self.notify_events = {}
+ self.subscribed = 0
+ self.add_notification_events(channel.notify_events)
+
+ silly = {
+ "What happen ?": "Somebody set up us the bomb.",
+ "It's You !!": ["How are you gentlemen !!",
+ "All your base are belong to us.",
+ "You are on the way to destruction."],
+ "What you say !!": ["You have no chance to survive make your time.",
+ "HA HA HA HA ...."],
+ }
+
+ def getCommandMethod(self, command):
+ meth = getattr(self, 'command_' + command.upper(), None)
+ return meth
+
+ def getBuilder(self, which):
+ try:
+ b = self.channel.status.getBuilder(which)
+ except KeyError:
+ raise UsageError, "no such builder '%s'" % which
+ return b
+
+ def getControl(self, which):
+ if not self.channel.control:
+ raise UsageError("builder control is not enabled")
+ try:
+ bc = self.channel.control.getBuilder(which)
+ except KeyError:
+ raise UsageError("no such builder '%s'" % which)
+ return bc
+
+ def getAllBuilders(self):
+ """
+ @rtype: list of L{buildbot.process.builder.Builder}
+ """
+ names = self.channel.status.getBuilderNames(categories=self.channel.categories)
+ names.sort()
+ builders = [self.channel.status.getBuilder(n) for n in names]
+ return builders
+
+ def convertTime(self, seconds):
+ if seconds < 60:
+ return "%d seconds" % seconds
+ minutes = int(seconds / 60)
+ seconds = seconds - 60*minutes
+ if minutes < 60:
+ return "%dm%02ds" % (minutes, seconds)
+ hours = int(minutes / 60)
+ minutes = minutes - 60*hours
+ return "%dh%02dm%02ds" % (hours, minutes, seconds)
+
+ 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.send, r)
+ when += 2.5
+
+ def command_HELLO(self, args, who):
+ self.send("yes?")
+
+ def command_VERSION(self, args, who):
+ self.send("buildbot-%s at your service" % version)
+
+ def command_LIST(self, args, who):
+ args = args.split()
+ if len(args) == 0:
+ raise UsageError, "try 'list builders'"
+ if args[0] == 'builders':
+ builders = self.getAllBuilders()
+ str = "Configured builders: "
+ for b in builders:
+ str += b.name
+ state = b.getState()[0]
+ if state == 'offline':
+ str += "[offline]"
+ str += " "
+ str.rstrip()
+ self.send(str)
+ return
+ command_LIST.usage = "list builders - List configured builders"
+
+ def command_STATUS(self, args, who):
+ args = args.split()
+ if len(args) == 0:
+ which = "all"
+ elif len(args) == 1:
+ which = args[0]
+ else:
+ raise UsageError, "try 'status <builder>'"
+ if which == "all":
+ builders = self.getAllBuilders()
+ for b in builders:
+ self.emit_status(b.name)
+ return
+ self.emit_status(which)
+ command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)"
+
+ def validate_notification_event(self, event):
+ if not re.compile("^(started|finished|success|failure|exception|warnings|(success|warnings|exception|failure)To(Failure|Success|Warnings|Exception))$").match(event):
+ raise UsageError("try 'notify on|off <EVENT>'")
+
+ def list_notified_events(self):
+ self.send( "The following events are being notified: %r" % self.notify_events.keys() )
+
+ def notify_for(self, *events):
+ for event in events:
+ if self.notify_events.has_key(event):
+ return 1
+ return 0
+
+ def subscribe_to_build_events(self):
+ self.channel.status.subscribe(self)
+ self.subscribed = 1
+
+ def unsubscribe_from_build_events(self):
+ self.channel.status.unsubscribe(self)
+ self.subscribed = 0
+
+ def add_notification_events(self, events):
+ for event in events:
+ self.validate_notification_event(event)
+ self.notify_events[event] = 1
+
+ if not self.subscribed:
+ self.subscribe_to_build_events()
+
+ def remove_notification_events(self, events):
+ for event in events:
+ self.validate_notification_event(event)
+ del self.notify_events[event]
+
+ if len(self.notify_events) == 0 and self.subscribed:
+ self.unsubscribe_from_build_events()
+
+ def remove_all_notification_events(self):
+ self.notify_events = {}
+
+ if self.subscribed:
+ self.unsubscribe_from_build_events()
+
+ def command_NOTIFY(self, args, who):
+ args = args.split()
+
+ if not args:
+ raise UsageError("try 'notify on|off|list <EVENT>'")
+ action = args.pop(0)
+ events = args
+
+ if action == "on":
+ if not events: events = ('started','finished')
+ self.add_notification_events(events)
+
+ self.list_notified_events()
+
+ elif action == "off":
+ if events:
+ self.remove_notification_events(events)
+ else:
+ self.remove_all_notification_events()
+
+ self.list_notified_events()
+
+ elif action == "list":
+ self.list_notified_events()
+ return
+
+ else:
+ raise UsageError("try 'notify on|off <EVENT>'")
+
+ command_NOTIFY.usage = "notify on|off|list [<EVENT>] ... - Notify me about build events. event should be one or more of: 'started', 'finished', 'failure', 'success', 'exception' or 'xToY' (where x and Y are one of success, warnings, failure, exception, but Y is capitalized)"
+
+ def command_WATCH(self, args, who):
+ args = args.split()
+ if len(args) != 1:
+ raise UsageError("try 'watch <builder>'")
+ which = args[0]
+ b = self.getBuilder(which)
+ builds = b.getCurrentBuilds()
+ if not builds:
+ self.send("there are no builds currently running")
+ return
+ for build in builds:
+ assert not build.isFinished()
+ d = build.waitUntilFinished()
+ d.addCallback(self.watchedBuildFinished)
+ 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.send(r)
+ command_WATCH.usage = "watch <which> - announce the completion of an active build"
+
+ def buildsetSubmitted(self, buildset):
+ log.msg('[Contact] Buildset %s added' % (buildset))
+
+ def builderAdded(self, builderName, builder):
+ log.msg('[Contact] Builder %s added' % (builder))
+ builder.subscribe(self)
+
+ def builderChangedState(self, builderName, state):
+ log.msg('[Contact] Builder %s changed state to %s' % (builderName, state))
+
+ def requestSubmitted(self, brstatus):
+ log.msg('[Contact] BuildRequest for %s submiitted to Builder %s' %
+ (brstatus.getSourceStamp(), brstatus.builderName))
+
+ def builderRemoved(self, builderName):
+ log.msg('[Contact] Builder %s removed' % (builderName))
+
+ def buildStarted(self, builderName, build):
+ builder = build.getBuilder()
+ log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category))
+
+ # only notify about builders we are interested in
+
+ if (self.channel.categories != None and
+ builder.category not in self.channel.categories):
+ log.msg('Not notifying for a build in the wrong category')
+ return
+
+ if not self.notify_for('started'):
+ log.msg('Not notifying for a build when started-notification disabled')
+ return
+
+ r = "build #%d of %s started" % \
+ (build.getNumber(),
+ builder.getName())
+
+ r += " including [" + ", ".join(map(lambda c: repr(c.revision), build.getChanges())) + "]"
+
+ self.send(r)
+
+ def buildFinished(self, builderName, build, results):
+ builder = build.getBuilder()
+
+ results_descriptions = {
+ SUCCESS: "Success",
+ WARNINGS: "Warnings",
+ FAILURE: "Failure",
+ EXCEPTION: "Exception",
+ }
+
+ # only notify about builders we are interested in
+ log.msg('[Contact] builder %r in category %s finished' % (builder, builder.category))
+
+ if self.notify_for('started'):
+ return
+
+ if (self.channel.categories != None and
+ builder.category not in self.channel.categories):
+ return
+
+ results = build.getResults()
+
+ r = "build #%d of %s is complete: %s" % \
+ (build.getNumber(),
+ builder.getName(),
+ results_descriptions.get(results, "??"))
+ r += " [%s]" % " ".join(build.getText())
+ buildurl = self.channel.status.getURLForThing(build)
+ if buildurl:
+ r += " Build details are at %s" % buildurl
+
+ if self.notify_for('finished') or self.notify_for(lower(results_descriptions.get(results))):
+ self.send(r)
+ return
+
+ prevBuild = build.getPreviousBuild()
+ if prevBuild:
+ prevResult = prevBuild.getResults()
+
+ required_notification_control_string = join((lower(results_descriptions.get(prevResult)), \
+ 'To', \
+ capitalize(results_descriptions.get(results))), \
+ '')
+
+ if (self.notify_for(required_notification_control_string)):
+ self.send(r)
+
+ def watchedBuildFinished(self, b):
+ results = {SUCCESS: "Success",
+ WARNINGS: "Warnings",
+ FAILURE: "Failure",
+ EXCEPTION: "Exception",
+ }
+
+ # only notify about builders we are interested in
+ builder = b.getBuilder()
+ log.msg('builder %r in category %s finished' % (builder,
+ builder.category))
+ if (self.channel.categories != None and
+ builder.category not in self.channel.categories):
+ return
+
+ r = "Hey! build %s #%d is complete: %s" % \
+ (b.getBuilder().getName(),
+ b.getNumber(),
+ results.get(b.getResults(), "??"))
+ r += " [%s]" % " ".join(b.getText())
+ self.send(r)
+ buildurl = self.channel.status.getURLForThing(b)
+ if buildurl:
+ self.send("Build details are at %s" % buildurl)
+
+ def command_FORCE(self, args, who):
+ args = shlex.split(args) # TODO: this requires python2.3 or newer
+ if not args:
+ raise UsageError("try 'force build WHICH <REASON>'")
+ what = args.pop(0)
+ if what != "build":
+ raise UsageError("try 'force build WHICH <REASON>'")
+ opts = ForceOptions()
+ opts.parseOptions(args)
+
+ which = opts['builder']
+ branch = opts['branch']
+ revision = opts['revision']
+ reason = opts['reason']
+
+ if which is None:
+ raise UsageError("you must provide a Builder, "
+ "try 'force build WHICH <REASON>'")
+
+ # keep weird stuff out of the branch and revision strings. TODO:
+ # centralize this somewhere.
+ if branch and not re.match(r'^[\w\.\-\/]*$', branch):
+ log.msg("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.send("sorry, bad revision '%s'" % revision)
+ return
+
+ bc = self.getControl(which)
+
+ r = "forced: by %s: %s" % (self.describeUser(who), reason)
+ # TODO: maybe give certain users the ability to request builds of
+ # certain branches
+ s = SourceStamp(branch=branch, revision=revision)
+ req = BuildRequest(r, s, which)
+ try:
+ bc.requestBuildSoon(req)
+ except interfaces.NoSlaveError:
+ self.send("sorry, I can't force a build: all slaves are offline")
+ return
+ ireq = IrcBuildRequest(self)
+ req.subscribe(ireq.started)
+
+
+ command_FORCE.usage = "force build <which> <reason> - Force a build"
+
+ def command_STOP(self, args, who):
+ args = args.split(None, 2)
+ if len(args) < 3 or args[0] != 'build':
+ raise UsageError, "try 'stop build WHICH <REASON>'"
+ which = args[1]
+ reason = args[2]
+
+ buildercontrol = self.getControl(which)
+
+ r = "stopped: by %s: %s" % (self.describeUser(who), reason)
+
+ # find an in-progress build
+ builderstatus = self.getBuilder(which)
+ builds = builderstatus.getCurrentBuilds()
+ if not builds:
+ self.send("sorry, no build is currently running")
+ return
+ for build in builds:
+ num = build.getNumber()
+
+ # obtain the BuildControl object
+ buildcontrol = buildercontrol.getBuild(num)
+
+ # make it stop
+ buildcontrol.stopBuild(r)
+
+ self.send("build %d interrupted" % num)
+
+ command_STOP.usage = "stop build <which> <reason> - Stop a running build"
+
+ def emit_status(self, which):
+ b = self.getBuilder(which)
+ str = "%s: " % which
+ state, builds = b.getState()
+ str += state
+ if state == "idle":
+ last = b.getLastFinishedBuild()
+ if last:
+ start,finished = last.getTimes()
+ str += ", last build %s ago: %s" % \
+ (self.convertTime(int(util.now() - finished)), " ".join(last.getText()))
+ if state == "building":
+ t = []
+ for build in builds:
+ step = build.getCurrentStep()
+ if step:
+ s = "(%s)" % " ".join(step.getText())
+ else:
+ s = "(no current step)"
+ ETA = build.getETA()
+ if ETA is not None:
+ s += " [ETA %s]" % self.convertTime(ETA)
+ t.append(s)
+ str += ", ".join(t)
+ self.send(str)
+
+ def emit_last(self, which):
+ last = self.getBuilder(which).getLastFinishedBuild()
+ if not last:
+ str = "(no builds run since last restart)"
+ else:
+ start,finish = last.getTimes()
+ str = "%s ago: " % (self.convertTime(int(util.now() - finish)))
+ str += " ".join(last.getText())
+ self.send("last build [%s]: %s" % (which, str))
+
+ def command_LAST(self, args, who):
+ args = args.split()
+ if len(args) == 0:
+ which = "all"
+ elif len(args) == 1:
+ which = args[0]
+ else:
+ raise UsageError, "try 'last <builder>'"
+ if which == "all":
+ builders = self.getAllBuilders()
+ for b in builders:
+ self.emit_last(b.name)
+ return
+ self.emit_last(which)
+ command_LAST.usage = "last <which> - list last build status for builder <which>"
+
+ def build_commands(self):
+ commands = []
+ for k in dir(self):
+ if k.startswith('command_'):
+ commands.append(k[8:].lower())
+ commands.sort()
+ return commands
+
+ def command_HELP(self, args, who):
+ args = args.split()
+ if len(args) == 0:
+ self.send("Get help on what? (try 'help <foo>', or 'commands' for a command list)")
+ return
+ command = args[0]
+ meth = self.getCommandMethod(command)
+ if not meth:
+ raise UsageError, "no such command '%s'" % command
+ usage = getattr(meth, 'usage', None)
+ if usage:
+ self.send("Usage: %s" % usage)
+ else:
+ self.send("No usage info for '%s'" % command)
+ command_HELP.usage = "help <command> - Give help for <command>"
+
+ def command_SOURCE(self, args, who):
+ banner = "My source can be found at http://buildbot.net/"
+ self.send(banner)
+
+ def command_COMMANDS(self, args, who):
+ commands = self.build_commands()
+ str = "buildbot commands: " + ", ".join(commands)
+ self.send(str)
+ command_COMMANDS.usage = "commands - List available commands"
+
+ def command_DESTROY(self, args, who):
+ self.act("readies phasers")
+
+ def command_DANCE(self, args, who):
+ 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, args, who):
+ # like 'buildbot: destroy the sun!'
+ 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)
+ # when people send us public messages ("buildbot: command"),
+ # self.dest is the name of the channel ("#twisted"). When they send
+ # us private messages (/msg buildbot command), self.dest is their
+ # username.
+ self.dest = dest
+
+ def describeUser(self, user):
+ if self.dest[0] == "#":
+ return "IRC user <%s> on channel %s" % (user, self.dest)
+ return "IRC user <%s> (privmsg)" % user
+
+ # userJoined(self, user, channel)
+
+ def send(self, message):
+ self.channel.msg(self.dest, message.encode("ascii", "replace"))
+ def act(self, action):
+ self.channel.me(self.dest, action.encode("ascii", "replace"))
+
+ def command_JOIN(self, args, who):
+ args = args.split()
+ to_join = args[0]
+ self.channel.join(to_join)
+ self.send("Joined %s" % to_join)
+ command_JOIN.usage = "join channel - Join another channel"
+
+ def command_LEAVE(self, args, who):
+ args = args.split()
+ to_leave = args[0]
+ self.send("Buildbot has been told to leave %s" % to_leave)
+ self.channel.part(to_leave)
+ command_LEAVE.usage = "leave channel - Leave a channel"
+
+
+ def handleMessage(self, message, who):
+ # a message has arrived from 'who'. For broadcast contacts (i.e. when
+ # people do an irc 'buildbot: command'), this will be a string
+ # describing the sender of the message in some useful-to-log way, and
+ # a single Contact may see messages from a variety of users. For
+ # unicast contacts (i.e. when people do an irc '/msg buildbot
+ # command'), a single Contact will only ever see messages from a
+ # single user.
+ 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(), who)
+ 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, notify_events):
+ """
+ @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.notify_events = notify_events
+ 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@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, user)
+ 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) or message.startswith("%s," % self.nickname):
+ message = message[len("%s:" % self.nickname):]
+ contact.handleMessage(message, user)
+ # 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@hostname
+ # 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
+ def clientConnectionLost(self, connector, reason):
+ reactor.callLater(self.lostDelay, connector.connect)
+ def clientConnectionFailed(self, connector, reason):
+ reactor.callLater(self.failedDelay, connector.connect)
+
+class IrcStatusFactory(ThrottledClientFactory):
+ protocol = IrcStatusBot
+
+ status = None
+ control = None
+ shuttingDown = False
+ p = None
+
+ def __init__(self, nickname, password, channels, categories, notify_events):
+ #ThrottledClientFactory.__init__(self) # doesn't exist
+ self.status = None
+ self.nickname = nickname
+ self.password = password
+ self.channels = channels
+ self.categories = categories
+ self.notify_events = notify_events
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ del d['p']
+ return d
+
+ def shutdown(self):
+ self.shuttingDown = True
+ if self.p:
+ self.p.quit("buildmaster reconfigured: bot disconnecting")
+
+ def buildProtocol(self, address):
+ p = self.protocol(self.nickname, self.password,
+ self.channels, self.status,
+ self.categories, self.notify_events)
+ p.factory = self
+ p.status = self.status
+ p.control = self.control
+ self.p = p
+ return p
+
+ # TODO: I think a shutdown that occurs while the connection is being
+ # established will make this explode
+
+ def clientConnectionLost(self, connector, reason):
+ if self.shuttingDown:
+ log.msg("not scheduling reconnection attempt")
+ return
+ ThrottledClientFactory.clientConnectionLost(self, connector, reason)
+
+ def clientConnectionFailed(self, connector, reason):
+ if self.shuttingDown:
+ log.msg("not scheduling reconnection attempt")
+ return
+ ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
+
+
+class IRC(base.StatusReceiverMultiService):
+ """I am an IRC bot which can be queried for status information. I
+ connect to a single IRC server and am known by a single nickname on that
+ server, however I can join multiple channels."""
+
+ compare_attrs = ["host", "port", "nick", "password",
+ "channels", "allowForce",
+ "categories"]
+
+ def __init__(self, host, nick, channels, port=6667, allowForce=True,
+ categories=None, password=None, notify_events={}):
+ base.StatusReceiverMultiService.__init__(self)
+
+ assert allowForce in (True, False) # TODO: implement others
+
+ # need to stash these so we can detect changes later
+ self.host = host
+ self.port = port
+ self.nick = nick
+ self.channels = channels
+ self.password = password
+ self.allowForce = allowForce
+ self.categories = categories
+ self.notify_events = notify_events
+
+ # need to stash the factory so we can give it the status object
+ self.f = IrcStatusFactory(self.nick, self.password,
+ self.channels, self.categories, self.notify_events)
+
+ c = internet.TCPClient(host, port, self.f)
+ c.setServiceParent(self)
+
+ def setServiceParent(self, parent):
+ base.StatusReceiverMultiService.setServiceParent(self, parent)
+ self.f.status = parent.getStatus()
+ if self.allowForce:
+ self.f.control = interfaces.IControl(parent)
+
+ def stopService(self):
+ # make sure the factory will stop reconnecting
+ self.f.shutdown()
+ return base.StatusReceiverMultiService.stopService(self)
+
+
+## buildbot: list builders
+# buildbot: watch quick
+# print notification when current build in 'quick' finishes
+## buildbot: status
+## buildbot: status full-2.3
+## building, not, % complete, ETA
+## buildbot: force build full-2.3 "reason"