Package buildbot :: Package status :: Module words
[frames] | no frames]

Source Code for Module buildbot.status.words

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