Add comment explaining why DIR's fd is OK
[xapian.git] / xapian-maintainer-tools / buildbot / words.py
blob62ca4fcf05e1014dfdc1f8694e37ebf455969002
1 # This file is part of Buildbot. Buildbot is free software: you can
2 # redistribute it and/or modify it under the terms of the GNU General Public
3 # License as published by the Free Software Foundation, version 2.
5 # This program is distributed in the hope that it will be useful, but WITHOUT
6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
8 # details.
10 # You should have received a copy of the GNU General Public License along with
11 # this program; if not, write to the Free Software Foundation, Inc., 51
12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
14 # Copyright Buildbot Team Members
16 import re, shlex, random
17 from string import join, capitalize, lower
19 from zope.interface import implements
20 from twisted.internet import protocol, reactor
21 from twisted.words.protocols import irc
22 from twisted.python import usage, log
23 from twisted.application import internet
24 from twisted.internet import defer, task
26 from buildbot import interfaces, util
27 from buildbot import version
28 from buildbot.interfaces import IStatusReceiver
29 from buildbot.sourcestamp import SourceStamp
30 from buildbot.status import base
31 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, EXCEPTION, RETRY
32 from buildbot.process.properties import Properties
34 # twisted.internet.ssl requires PyOpenSSL, so be resilient if it's missing
35 try:
36 from twisted.internet import ssl
37 have_ssl = True
38 except ImportError:
39 have_ssl = False
41 def maybeColorize(text, color, useColors):
42 irc_colors = [
43 'WHITE',
44 'BLACK',
45 'NAVY_BLUE',
46 'GREEN',
47 'RED',
48 'BROWN',
49 'PURPLE',
50 'OLIVE',
51 'YELLOW',
52 'LIME_GREEN',
53 'TEAL',
54 'AQUA_LIGHT',
55 'ROYAL_BLUE',
56 'HOT_PINK',
57 'DARK_GRAY',
58 'LIGHT_GRAY'
61 if useColors:
62 return "%c%d%s%c" % (3, irc_colors.index(color), text, 3)
63 else:
64 return text
66 class UsageError(ValueError):
67 def __init__(self, string = "Invalid usage", *more):
68 ValueError.__init__(self, string, *more)
70 class ForceOptions(usage.Options):
71 optParameters = [
72 ["builder", None, None, "which Builder to start"],
73 ["branch", None, None, "which branch to build"],
74 ["revision", None, None, "which revision to build"],
75 ["reason", None, None, "the reason for starting the build"],
76 ["props", None, None,
77 "A set of properties made available in the build environment, "
78 "format is --properties=prop1=value1,prop2=value2,.. "
79 "option can be specified multiple times."],
82 def parseArgs(self, *args):
83 args = list(args)
84 if len(args) > 0:
85 if self['builder'] is not None:
86 raise UsageError("--builder provided in two ways")
87 self['builder'] = args.pop(0)
88 if len(args) > 0:
89 if self['reason'] is not None:
90 raise UsageError("--reason provided in two ways")
91 self['reason'] = " ".join(args)
94 class IrcBuildRequest:
95 hasStarted = False
96 timer = None
98 def __init__(self, parent, useRevisions=False, useColors=True):
99 self.parent = parent
100 self.useRevisions = useRevisions
101 self.useColors = useColors
102 self.timer = reactor.callLater(5, self.soon)
104 def soon(self):
105 del self.timer
106 if not self.hasStarted:
107 self.parent.send("The build has been queued, I'll give a shout"
108 " when it starts")
110 def started(self, s):
111 self.hasStarted = True
112 if self.timer:
113 self.timer.cancel()
114 del self.timer
115 eta = s.getETA()
116 if self.useRevisions:
117 response = "build containing revision(s) [%s] forced" % s.getRevisions()
118 else:
119 response = "build #%d forced" % s.getNumber()
120 if eta is not None:
121 response = "build forced [ETA %s]" % self.parent.convertTime(eta)
122 self.parent.send(response)
123 self.parent.send("I will politely inform you when the build finishes")
124 d = s.waitUntilFinished()
125 d.addCallback(self.parent.watchedBuildFinished)
127 class IRCContact(base.StatusReceiver):
128 implements(IStatusReceiver)
129 """I hold the state for a single user's interaction with the buildbot.
131 There will be one instance of me for each user who interacts personally
132 with the buildbot. There will be an additional instance for each
133 'broadcast contact' (chat rooms, IRC channels as a whole).
136 def __init__(self, bot, dest):
137 self.bot = bot
138 self.master = bot.master
139 self.notify_events = {}
140 self.subscribed = 0
141 self.muted = False
142 self.useRevisions = bot.useRevisions
143 self.useColors = bot.useColors
144 self.reported_builds = [] # tuples (when, buildername, buildnum)
145 self.add_notification_events(bot.notify_events)
147 # when people send us public messages ("buildbot: command"),
148 # self.dest is the name of the channel ("#twisted"). When they send
149 # us private messages (/msg buildbot command), self.dest is their
150 # username.
151 self.dest = dest
153 # silliness
155 silly = {
156 "What happen ?": [ "Somebody set up us the bomb." ],
157 "It's You !!": ["How are you gentlemen !!",
158 "All your base are belong to us.",
159 "You are on the way to destruction."],
160 "What you say !!": ["You have no chance to survive make your time.",
161 "HA HA HA HA ...."],
164 def doSilly(self, message):
165 response = self.silly[message]
166 when = 0.5
167 for r in response:
168 reactor.callLater(when, self.send, r)
169 when += 2.5
171 def getBuilder(self, which):
172 try:
173 b = self.bot.status.getBuilder(which)
174 except KeyError:
175 raise UsageError, "no such builder '%s'" % which
176 return b
178 def getControl(self, which):
179 if not self.bot.control:
180 raise UsageError("builder control is not enabled")
181 try:
182 bc = self.bot.control.getBuilder(which)
183 except KeyError:
184 raise UsageError("no such builder '%s'" % which)
185 return bc
187 def getAllBuilders(self):
189 @rtype: list of L{buildbot.process.builder.Builder}
191 names = self.bot.status.getBuilderNames(categories=self.bot.categories)
192 names.sort()
193 builders = [self.bot.status.getBuilder(n) for n in names]
194 return builders
196 def convertTime(self, seconds):
197 if seconds < 60:
198 return "%d seconds" % seconds
199 minutes = int(seconds / 60)
200 seconds = seconds - 60*minutes
201 if minutes < 60:
202 return "%dm%02ds" % (minutes, seconds)
203 hours = int(minutes / 60)
204 minutes = minutes - 60*hours
205 return "%dh%02dm%02ds" % (hours, minutes, seconds)
207 def reportBuild(self, builder, buildnum):
208 """Returns True if this build should be reported for this contact
209 (eliminating duplicates), and also records the report for later"""
210 for w, b, n in self.reported_builds:
211 if b == builder and n == buildnum:
212 return False
213 self.reported_builds.append([util.now(), builder, buildnum])
215 # clean the reported builds
216 horizon = util.now() - 60
217 while self.reported_builds and self.reported_builds[0][0] < horizon:
218 self.reported_builds.pop(0)
220 # and return True, since this is a new one
221 return True
223 def command_HELLO(self, args, who):
224 self.send("yes?")
226 def command_VERSION(self, args, who):
227 self.send("buildbot-%s at your service" % version)
229 def command_LIST(self, args, who):
230 args = shlex.split(args)
231 if len(args) == 0:
232 raise UsageError, "try 'list builders'"
233 if args[0] == 'builders':
234 builders = self.getAllBuilders()
235 str = "Configured builders: "
236 for b in builders:
237 str += b.name
238 state = b.getState()[0]
239 if state == 'offline':
240 str += "[offline]"
241 str += " "
242 str.rstrip()
243 self.send(str)
244 return
245 command_LIST.usage = "list builders - List configured builders"
247 def command_STATUS(self, args, who):
248 args = shlex.split(args)
249 if len(args) == 0:
250 which = "all"
251 elif len(args) == 1:
252 which = args[0]
253 else:
254 raise UsageError, "try 'status <builder>'"
255 if which == "all":
256 builders = self.getAllBuilders()
257 for b in builders:
258 self.emit_status(b.name)
259 return
260 self.emit_status(which)
261 command_STATUS.usage = "status [<which>] - List status of a builder (or all builders)"
263 def validate_notification_event(self, event):
264 if not re.compile("^(started|finished|success|failure|exception|warnings|(success|warnings|exception|failure)To(Failure|Success|Warnings|Exception))$").match(event):
265 raise UsageError("try 'notify on|off <EVENT>'")
267 def list_notified_events(self):
268 self.send( "The following events are being notified: %r" % self.notify_events.keys() )
270 def notify_for(self, *events):
271 for event in events:
272 if self.notify_events.has_key(event):
273 return 1
274 return 0
276 def subscribe_to_build_events(self):
277 self.bot.status.subscribe(self)
278 self.subscribed = 1
280 def unsubscribe_from_build_events(self):
281 self.bot.status.unsubscribe(self)
282 self.subscribed = 0
284 def add_notification_events(self, events):
285 for event in events:
286 self.validate_notification_event(event)
287 self.notify_events[event] = 1
289 if not self.subscribed:
290 self.subscribe_to_build_events()
292 def remove_notification_events(self, events):
293 for event in events:
294 self.validate_notification_event(event)
295 del self.notify_events[event]
297 if len(self.notify_events) == 0 and self.subscribed:
298 self.unsubscribe_from_build_events()
300 def remove_all_notification_events(self):
301 self.notify_events = {}
303 if self.subscribed:
304 self.unsubscribe_from_build_events()
306 def command_NOTIFY(self, args, who):
307 args = shlex.split(args)
309 if not args:
310 raise UsageError("try 'notify on|off|list <EVENT>'")
311 action = args.pop(0)
312 events = args
314 if action == "on":
315 if not events: events = ('started','finished')
316 self.add_notification_events(events)
318 self.list_notified_events()
320 elif action == "off":
321 if events:
322 self.remove_notification_events(events)
323 else:
324 self.remove_all_notification_events()
326 self.list_notified_events()
328 elif action == "list":
329 self.list_notified_events()
330 return
332 else:
333 raise UsageError("try 'notify on|off <EVENT>'")
335 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)"
337 def command_WATCH(self, args, who):
338 args = shlex.split(args)
339 if len(args) != 1:
340 raise UsageError("try 'watch <builder>'")
341 which = args[0]
342 b = self.getBuilder(which)
343 builds = b.getCurrentBuilds()
344 if not builds:
345 self.send("there are no builds currently running")
346 return
347 for build in builds:
348 assert not build.isFinished()
349 d = build.waitUntilFinished()
350 d.addCallback(self.watchedBuildFinished)
351 if self.useRevisions:
352 r = "watching build %s containing revision(s) [%s] until it finishes" \
353 % (which, build.getRevisions())
354 else:
355 r = "watching build %s #%d until it finishes" \
356 % (which, build.getNumber())
357 eta = build.getETA()
358 if eta is not None:
359 r += " [%s]" % self.convertTime(eta)
360 r += ".."
361 self.send(r)
362 command_WATCH.usage = "watch <which> - announce the completion of an active build"
364 def builderAdded(self, builderName, builder):
365 if (self.bot.categories != None and
366 builder.category not in self.bot.categories):
367 return
369 log.msg('[Contact] Builder %s added' % (builder))
370 builder.subscribe(self)
372 def builderRemoved(self, builderName):
373 log.msg('[Contact] Builder %s removed' % (builderName))
375 def buildStarted(self, builderName, build):
376 builder = build.getBuilder()
377 log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category))
379 # only notify about builders we are interested in
381 if (self.bot.categories != None and
382 builder.category not in self.bot.categories):
383 log.msg('Not notifying for a build in the wrong category')
384 return
386 if not self.notify_for('started'):
387 return
389 if self.useRevisions:
390 r = "build containing revision(s) [%s] on %s started" % \
391 (build.getRevisions(), builder.getName())
392 else:
393 r = "build #%d of %s started, including [%s]" % \
394 (build.getNumber(),
395 builder.getName(),
396 ", ".join([str(c.revision) for c in build.getChanges()])
399 self.send(r)
401 results_descriptions = {
402 SUCCESS: ("Success", 'GREEN'),
403 WARNINGS: ("Warnings", 'YELLOW'),
404 FAILURE: ("Failure", 'RED'),
405 EXCEPTION: ("Exception", 'PURPLE'),
406 RETRY: ("Retry", 'AQUA_LIGHT'),
409 def getResultsDescriptionAndColor(self, results):
410 return self.results_descriptions.get(results, ("??",'RED'))
412 def buildFinished(self, builderName, build, results):
413 builder = build.getBuilder()
415 if (self.bot.categories != None and
416 builder.category not in self.bot.categories):
417 return
419 if not self.notify_for_finished(build):
420 return
422 builder_name = builder.getName()
423 buildnum = build.getNumber()
424 buildrevs = build.getRevisions()
426 results = self.getResultsDescriptionAndColor(build.getResults())
427 if self.reportBuild(builder_name, buildnum):
428 if self.useRevisions:
429 r = "build containing revision(s) [%s] on %s is complete: %s" % \
430 (buildrevs, builder_name, results[0])
431 else:
432 r = "build #%d of %s is complete: %s" % \
433 (buildnum, builder_name, results[0])
435 r += ' [%s]' % maybeColorize(" ".join(build.getText()), results[1], self.useColors)
436 buildurl = self.bot.status.getURLForThing(build)
437 if buildurl:
438 r += " Build details are at %s" % buildurl
440 if self.bot.showBlameList and build.getResults() != SUCCESS and len(build.changes) != 0:
441 r += ' blamelist: ' + ', '.join(list(set([c.who for c in build.changes])))
443 self.send(r)
445 def notify_for_finished(self, build):
446 results = build.getResults()
448 if self.notify_for('finished'):
449 return True
451 if self.notify_for(lower(self.results_descriptions.get(results)[0])):
452 return True
454 prevBuild = build.getPreviousBuild()
455 if prevBuild:
456 prevResult = prevBuild.getResults()
458 required_notification_control_string = join((lower(self.results_descriptions.get(prevResult)[0]), \
459 'To', \
460 capitalize(self.results_descriptions.get(results)[0])), \
463 if (self.notify_for(required_notification_control_string)):
464 return True
466 return False
468 def watchedBuildFinished(self, b):
470 # only notify about builders we are interested in
471 builder = b.getBuilder()
472 if (self.bot.categories != None and
473 builder.category not in self.bot.categories):
474 return
476 builder_name = builder.getName()
477 buildnum = b.getNumber()
478 buildrevs = b.getRevisions()
480 results = self.getResultsDescriptionAndColor(b.getResults())
481 if self.reportBuild(builder_name, buildnum):
482 if self.useRevisions:
483 r = "Hey! build %s containing revision(s) [%s] is complete: %s" % \
484 (builder_name, buildrevs, results[0])
485 else:
486 r = "Hey! build %s #%d is complete: %s" % \
487 (builder_name, buildnum, results[0])
489 r += ' [%s]' % maybeColorize(" ".join(b.getText()), results[1], self.useColors)
490 self.send(r)
491 buildurl = self.bot.status.getURLForThing(b)
492 if buildurl:
493 self.send("Build details are at %s" % buildurl)
495 def command_FORCE(self, args, who):
496 errReply = "try 'force build [--branch=BRANCH] [--revision=REVISION] [--props=PROP1=VAL1,PROP2=VAL2...] <WHICH> <REASON>'"
497 args = shlex.split(args)
498 if not args:
499 raise UsageError(errReply)
500 what = args.pop(0)
501 if what != "build":
502 raise UsageError(errReply)
503 opts = ForceOptions()
504 opts.parseOptions(args)
506 which = opts['builder']
507 branch = opts['branch']
508 revision = opts['revision']
509 reason = opts['reason']
510 props = opts['props']
512 if which is None:
513 raise UsageError("you must provide a Builder, " + errReply)
515 # keep weird stuff out of the branch, revision, and properties args.
516 branch_validate = self.master.config.validation['branch']
517 revision_validate = self.master.config.validation['revision']
518 pname_validate = self.master.config.validation['property_name']
519 pval_validate = self.master.config.validation['property_value']
520 if branch and not branch_validate.match(branch):
521 log.msg("bad branch '%s'" % branch)
522 self.send("sorry, bad branch '%s'" % branch)
523 return
524 if revision and not revision_validate.match(revision):
525 log.msg("bad revision '%s'" % revision)
526 self.send("sorry, bad revision '%s'" % revision)
527 return
529 properties = Properties()
530 if props:
531 # split props into name:value dict
532 pdict = {}
533 propertylist = props.split(",")
534 for i in range(0,len(propertylist)):
535 splitproperty = propertylist[i].split("=", 1)
536 pdict[splitproperty[0]] = splitproperty[1]
538 # set properties
539 for prop in pdict:
540 pname = prop
541 pvalue = pdict[prop]
542 if not pname_validate.match(pname) \
543 or not pval_validate.match(pvalue):
544 log.msg("bad property name='%s', value='%s'" % (pname, pvalue))
545 self.send("sorry, bad property name='%s', value='%s'" %
546 (pname, pvalue))
547 return
548 properties.setProperty(pname, pvalue, "Force Build IRC")
550 bc = self.getControl(which)
552 reason = "forced: by %s: %s" % (self.describeUser(who), reason)
553 ss = SourceStamp(branch=branch, revision=revision)
554 d = bc.submitBuildRequest(ss, reason, props=properties.asDict())
555 def subscribe(buildreq):
556 ireq = IrcBuildRequest(self, self.useRevisions)
557 buildreq.subscribe(ireq.started)
558 d.addCallback(subscribe)
559 d.addErrback(log.err, "while forcing a build")
562 command_FORCE.usage = "force build [--branch=branch] [--revision=revision] [--props=prop1=val1,prop2=val2...] <which> <reason> - Force a build"
564 def command_STOP(self, args, who):
565 args = shlex.split(args)
566 if len(args) < 3 or args[0] != 'build':
567 raise UsageError, "try 'stop build WHICH <REASON>'"
568 which = args[1]
569 reason = args[2]
571 buildercontrol = self.getControl(which)
573 r = "stopped: by %s: %s" % (self.describeUser(who), reason)
575 # find an in-progress build
576 builderstatus = self.getBuilder(which)
577 builds = builderstatus.getCurrentBuilds()
578 if not builds:
579 self.send("sorry, no build is currently running")
580 return
581 for build in builds:
582 num = build.getNumber()
583 revs = build.getRevisions()
585 # obtain the BuildControl object
586 buildcontrol = buildercontrol.getBuild(num)
588 # make it stop
589 buildcontrol.stopBuild(r)
591 if self.useRevisions:
592 response = "build containing revision(s) [%s] interrupted" % revs
593 else:
594 response = "build %d interrupted" % num
595 self.send(response)
597 command_STOP.usage = "stop build <which> <reason> - Stop a running build"
599 def emit_status(self, which):
600 b = self.getBuilder(which)
601 str = "%s: " % which
602 state, builds = b.getState()
603 str += state
604 if state == "idle":
605 last = b.getLastFinishedBuild()
606 if last:
607 start,finished = last.getTimes()
608 str += ", last build %s ago: %s" % \
609 (self.convertTime(int(util.now() - finished)), " ".join(last.getText()))
610 if state == "building":
611 t = []
612 for build in builds:
613 step = build.getCurrentStep()
614 if step:
615 s = "(%s)" % " ".join(step.getText())
616 else:
617 s = "(no current step)"
618 ETA = build.getETA()
619 if ETA is not None:
620 s += " [ETA %s]" % self.convertTime(ETA)
621 t.append(s)
622 str += ", ".join(t)
623 self.send(str)
625 def command_LAST(self, args, who):
626 args = shlex.split(args)
628 if len(args) == 0:
629 which = "all"
630 elif len(args) == 1:
631 which = args[0]
632 else:
633 raise UsageError, "try 'last <builder>'"
635 def emit_last(which):
636 last = self.getBuilder(which).getLastFinishedBuild()
637 if not last:
638 str = "(no builds run since last restart)"
639 else:
640 start,finish = last.getTimes()
641 str = "%s ago: " % (self.convertTime(int(util.now() - finish)))
642 str += " ".join(last.getText())
643 self.send("last build [%s]: %s" % (which, str))
645 if which == "all":
646 builders = self.getAllBuilders()
647 for b in builders:
648 emit_last(b.name)
649 return
650 emit_last(which)
651 command_LAST.usage = "last <which> - list last build status for builder <which>"
653 def build_commands(self):
654 commands = []
655 for k in dir(self):
656 if k.startswith('command_'):
657 commands.append(k[8:].lower())
658 commands.sort()
659 return commands
661 def describeUser(self, user):
662 if self.dest[0] == '#':
663 return "IRC user <%s> on channel %s" % (user, self.dest)
664 return "IRC user <%s> (privmsg)" % user
666 # commands
668 def command_MUTE(self, args, who):
669 # The order of these is important! ;)
670 self.send("Shutting up for now.")
671 self.muted = True
672 command_MUTE.usage = "mute - suppress all messages until a corresponding 'unmute' is issued"
674 def command_UNMUTE(self, args, who):
675 if self.muted:
676 # The order of these is important! ;)
677 self.muted = False
678 self.send("I'm baaaaaaaaaaack!")
679 else:
680 self.send("You hadn't told me to be quiet, but it's the thought that counts, right?")
681 command_UNMUTE.usage = "unmute - disable a previous 'mute'"
683 def command_HELP(self, args, who):
684 args = shlex.split(args)
685 if len(args) == 0:
686 self.send("Get help on what? (try 'help <foo>', "
687 "or 'commands' for a command list)")
688 return
689 command = args[0]
690 meth = self.getCommandMethod(command)
691 if not meth:
692 raise UsageError, "no such command '%s'" % command
693 usage = getattr(meth, 'usage', None)
694 if usage:
695 self.send("Usage: %s" % usage)
696 else:
697 self.send("No usage info for '%s'" % command)
698 command_HELP.usage = "help <command> - Give help for <command>"
700 def command_SOURCE(self, args, who):
701 self.send("My source can be found at "
702 "https://github.com/buildbot/buildbot")
703 command_SOURCE.usage = "source - the source code for Buildbot"
705 def command_COMMANDS(self, args, who):
706 commands = self.build_commands()
707 str = "buildbot commands: " + ", ".join(commands)
708 self.send(str)
709 command_COMMANDS.usage = "commands - List available commands"
711 def command_DESTROY(self, args, who):
712 self.act("readies phasers")
714 def command_DANCE(self, args, who):
715 return
716 reactor.callLater(1.0, self.send, "<(^.^<)")
717 reactor.callLater(2.0, self.send, "<(^.^)>")
718 reactor.callLater(3.0, self.send, "(>^.^)>")
719 reactor.callLater(3.5, self.send, "(7^.^)7")
720 reactor.callLater(5.0, self.send, "(>^.^<)")
722 # communication with the user
724 def send(self, message):
725 if not self.muted:
726 self.bot.msgOrNotice(self.dest, message.encode("ascii", "replace"))
728 def act(self, action):
729 if not self.muted:
730 self.bot.describe(self.dest, action.encode("ascii", "replace"))
732 # main dispatchers for incoming messages
734 def getCommandMethod(self, command):
735 return getattr(self, 'command_' + command.upper(), None)
737 def handleMessage(self, message, who):
738 # a message has arrived from 'who'. For broadcast contacts (i.e. when
739 # people do an irc 'buildbot: command'), this will be a string
740 # describing the sender of the message in some useful-to-log way, and
741 # a single Contact may see messages from a variety of users. For
742 # unicast contacts (i.e. when people do an irc '/msg buildbot
743 # command'), a single Contact will only ever see messages from a
744 # single user.
745 message = message.lstrip()
746 #if self.silly.has_key(message):
747 # self.doSilly(message)
748 # return defer.succeed(None)
750 parts = message.split(' ', 1)
751 if len(parts) == 1:
752 parts = parts + ['']
753 cmd, args = parts
754 log.msg("irc command", cmd)
756 meth = self.getCommandMethod(cmd)
757 if not meth and message[-1] == '!':
758 self.send("What you say!")
759 return defer.succeed(None)
761 if meth:
762 d = defer.maybeDeferred(meth, args.strip(), who)
763 @d.addErrback
764 def usageError(f):
765 f.trap(UsageError)
766 self.send(str(f.value))
767 @d.addErrback
768 def logErr(f):
769 log.err(f)
770 self.send("Something bad happened (see logs)")
771 d.addErrback(log.err)
772 return d
773 return defer.succeed(None)
775 def handleAction(self, data, user):
776 return
777 # this is sent when somebody performs an action that mentions the
778 # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of
779 # the person who performed the action, so if their action provokes a
780 # response, they can be named. This is 100% silly.
781 if not data.endswith("s "+ self.bot.nickname):
782 return
783 words = data.split()
784 verb = words[-2]
785 if verb == "kicks":
786 response = "%s back" % verb
787 else:
788 response = "%s %s too" % (verb, user)
789 self.act(response)
792 class IrcStatusBot(irc.IRCClient):
793 """I represent the buildbot to an IRC server.
795 contactClass = IRCContact
797 def __init__(self, nickname, password, channels, pm_to_nicks, status,
798 categories, notify_events, noticeOnChannel=False,
799 useRevisions=False, showBlameList=False, useColors=True):
800 self.nickname = nickname
801 self.channels = channels
802 self.pm_to_nicks = pm_to_nicks
803 self.password = password
804 self.status = status
805 self.master = status.master
806 self.categories = categories
807 self.notify_events = notify_events
808 self.hasQuit = 0
809 self.contacts = {}
810 self.noticeOnChannel = noticeOnChannel
811 self.useColors = useColors
812 self.useRevisions = useRevisions
813 self.showBlameList = showBlameList
814 self._keepAliveCall = task.LoopingCall(lambda: self.ping(self.nickname))
816 def connectionMade(self):
817 irc.IRCClient.connectionMade(self)
818 self._keepAliveCall.start(60)
820 def connectionLost(self, reason):
821 if self._keepAliveCall.running:
822 self._keepAliveCall.stop()
823 irc.IRCClient.connectionLost(self, reason)
825 def msgOrNotice(self, dest, message):
826 if self.noticeOnChannel and dest[0] == '#':
827 self.notice(dest, message)
828 else:
829 self.msg(dest, message)
831 def getContact(self, name):
832 name = name.lower() # nicknames and channel names are case insensitive
833 if name in self.contacts:
834 return self.contacts[name]
835 new_contact = self.contactClass(self, name)
836 self.contacts[name] = new_contact
837 return new_contact
839 def log(self, msg):
840 log.msg("%s: %s" % (self, msg))
843 # the following irc.IRCClient methods are called when we have input
845 def privmsg(self, user, channel, message):
846 user = user.split('!', 1)[0] # rest is ~user@hostname
847 # channel is '#twisted' or 'buildbot' (for private messages)
848 if channel == self.nickname:
849 # private message
850 contact = self.getContact(user)
851 contact.handleMessage(message, user)
852 return
853 # else it's a broadcast message, maybe for us, maybe not. 'channel'
854 # is '#twisted' or the like.
855 contact = self.getContact(channel)
856 if message.startswith("%s:" % self.nickname) or message.startswith("%s," % self.nickname):
857 message = message[len("%s:" % self.nickname):]
858 contact.handleMessage(message, user)
860 def action(self, user, channel, data):
861 user = user.split('!', 1)[0] # rest is ~user@hostname
862 # somebody did an action (/me actions) in the broadcast channel
863 contact = self.getContact(channel)
864 if self.nickname in data:
865 contact.handleAction(data, user)
867 def signedOn(self):
868 if self.password:
869 self.msg("Nickserv", "IDENTIFY " + self.password)
870 for c in self.channels:
871 if isinstance(c, dict):
872 channel = c.get('channel', None)
873 password = c.get('password', None)
874 else:
875 channel = c
876 password = None
877 self.join(channel=channel, key=password)
878 for c in self.pm_to_nicks:
879 self.getContact(c)
881 def joined(self, channel):
882 self.log("I have joined %s" % (channel,))
883 # trigger contact constructor, which in turn subscribes to notify events
884 self.getContact(channel)
886 def left(self, channel):
887 self.log("I have left %s" % (channel,))
889 def kickedFrom(self, channel, kicker, message):
890 self.log("I have been kicked from %s by %s: %s" % (channel,
891 kicker,
892 message))
895 class ThrottledClientFactory(protocol.ClientFactory):
896 lostDelay = random.randint(1, 5)
897 failedDelay = random.randint(45, 60)
899 def __init__(self, lostDelay=None, failedDelay=None):
900 if lostDelay is not None:
901 self.lostDelay = lostDelay
902 if failedDelay is not None:
903 self.failedDelay = failedDelay
905 def clientConnectionLost(self, connector, reason):
906 reactor.callLater(self.lostDelay, connector.connect)
908 def clientConnectionFailed(self, connector, reason):
909 reactor.callLater(self.failedDelay, connector.connect)
912 class IrcStatusFactory(ThrottledClientFactory):
913 protocol = IrcStatusBot
915 status = None
916 control = None
917 shuttingDown = False
918 p = None
920 def __init__(self, nickname, password, channels, pm_to_nicks, categories, notify_events,
921 noticeOnChannel=False, useRevisions=False, showBlameList=False,
922 lostDelay=None, failedDelay=None, useColors=True):
923 ThrottledClientFactory.__init__(self, lostDelay=lostDelay,
924 failedDelay=failedDelay)
925 self.status = None
926 self.nickname = nickname
927 self.password = password
928 self.channels = channels
929 self.pm_to_nicks = pm_to_nicks
930 self.categories = categories
931 self.notify_events = notify_events
932 self.noticeOnChannel = noticeOnChannel
933 self.useRevisions = useRevisions
934 self.showBlameList = showBlameList
935 self.useColors = useColors
937 def __getstate__(self):
938 d = self.__dict__.copy()
939 del d['p']
940 return d
942 def shutdown(self):
943 self.shuttingDown = True
944 if self.p:
945 self.p.quit("buildmaster reconfigured: bot disconnecting")
947 def buildProtocol(self, address):
948 p = self.protocol(self.nickname, self.password,
949 self.channels, self.pm_to_nicks, self.status,
950 self.categories, self.notify_events,
951 noticeOnChannel = self.noticeOnChannel,
952 useColors = self.useColors,
953 useRevisions = self.useRevisions,
954 showBlameList = self.showBlameList)
955 p.factory = self
956 p.status = self.status
957 p.control = self.control
958 self.p = p
959 return p
961 # TODO: I think a shutdown that occurs while the connection is being
962 # established will make this explode
964 def clientConnectionLost(self, connector, reason):
965 if self.shuttingDown:
966 log.msg("not scheduling reconnection attempt")
967 return
968 ThrottledClientFactory.clientConnectionLost(self, connector, reason)
970 def clientConnectionFailed(self, connector, reason):
971 if self.shuttingDown:
972 log.msg("not scheduling reconnection attempt")
973 return
974 ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
977 class IRC(base.StatusReceiverMultiService):
978 implements(IStatusReceiver)
980 in_test_harness = False
982 compare_attrs = ["host", "port", "nick", "password",
983 "channels", "pm_to_nicks", "allowForce", "useSSL",
984 "useRevisions", "categories", "useColors",
985 "lostDelay", "failedDelay"]
987 def __init__(self, host, nick, channels, pm_to_nicks=[], port=6667,
988 allowForce=False, categories=None, password=None, notify_events={},
989 noticeOnChannel = False, showBlameList = True, useRevisions=False,
990 useSSL=False, lostDelay=None, failedDelay=None, useColors=True):
991 base.StatusReceiverMultiService.__init__(self)
993 assert allowForce in (True, False) # TODO: implement others
995 # need to stash these so we can detect changes later
996 self.host = host
997 self.port = port
998 self.nick = nick
999 self.channels = channels
1000 self.pm_to_nicks = pm_to_nicks
1001 self.password = password
1002 self.allowForce = allowForce
1003 self.useRevisions = useRevisions
1004 self.categories = categories
1005 self.notify_events = notify_events
1007 self.f = IrcStatusFactory(self.nick, self.password,
1008 self.channels, self.pm_to_nicks,
1009 self.categories, self.notify_events,
1010 noticeOnChannel = noticeOnChannel,
1011 useRevisions = useRevisions,
1012 showBlameList = showBlameList,
1013 lostDelay = lostDelay,
1014 failedDelay = failedDelay,
1015 useColors = useColors)
1017 if useSSL:
1018 # SSL client needs a ClientContextFactory for some SSL mumbo-jumbo
1019 if not have_ssl:
1020 raise RuntimeError("useSSL requires PyOpenSSL")
1021 cf = ssl.ClientContextFactory()
1022 c = internet.SSLClient(self.host, self.port, self.f, cf)
1023 else:
1024 c = internet.TCPClient(self.host, self.port, self.f)
1026 c.setServiceParent(self)
1028 def setServiceParent(self, parent):
1029 base.StatusReceiverMultiService.setServiceParent(self, parent)
1030 self.f.status = parent
1031 if self.allowForce:
1032 self.f.control = interfaces.IControl(self.master)
1034 def stopService(self):
1035 # make sure the factory will stop reconnecting
1036 self.f.shutdown()
1037 return base.StatusReceiverMultiService.stopService(self)