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 usage, log 
  23  from twisted.application import internet 
  24  from twisted.internet import defer, 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.process.properties import Properties 
  33   
  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 
40 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' 59 ] 60 61 if useColors: 62 return "%c%d%s%c" % (3, irc_colors.index(color), text, 3) 63 else: 64 return text
65
66 -class UsageError(ValueError):
67 - def __init__(self, string = "Invalid usage", *more):
68 ValueError.__init__(self, string, *more)
69
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."], 80 ] 81
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)
92
93 94 -class IrcBuildRequest:
95 hasStarted = False 96 timer = None 97
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)
103
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")
109
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'll give a shout when the build finishes") 124 d = s.waitUntilFinished() 125 d.addCallback(self.parent.watchedBuildFinished)
126
127 -class IRCContact(base.StatusReceiver):
128 implements(IStatusReceiver) 129 """I hold the state for a single user's interaction with the buildbot. 130 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). 134 """ 135
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) 146 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
152 153 # silliness 154 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 ...."], 162 } 163
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
170
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
177
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
186
187 - def getAllBuilders(self):
188 """ 189 @rtype: list of L{buildbot.process.builder.Builder} 190 """ 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
195
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)
206
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]) 214 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) 219 220 # and return True, since this is a new one 221 return True
222
223 - def command_HELLO(self, args, who):
224 self.send("yes?")
225
226 - def command_VERSION(self, args, who):
227 self.send("buildbot-%s at your service" % version)
228
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" 246
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)" 262
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>'")
266
267 - def list_notified_events(self):
268 self.send( "The following events are being notified: %r" % self.notify_events.keys() )
269
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
275
276 - def subscribe_to_build_events(self):
277 self.bot.status.subscribe(self) 278 self.subscribed = 1
279
281 self.bot.status.unsubscribe(self) 282 self.subscribed = 0
283
284 - def add_notification_events(self, events):
285 for event in events: 286 self.validate_notification_event(event) 287 self.notify_events[event] = 1 288 289 if not self.subscribed: 290 self.subscribe_to_build_events()
291
292 - def remove_notification_events(self, events):
293 for event in events: 294 self.validate_notification_event(event) 295 del self.notify_events[event] 296 297 if len(self.notify_events) == 0 and self.subscribed: 298 self.unsubscribe_from_build_events()
299
301 self.notify_events = {} 302 303 if self.subscribed: 304 self.unsubscribe_from_build_events()
305
306 - def command_NOTIFY(self, args, who):
307 args = shlex.split(args) 308 309 if not args: 310 raise UsageError("try 'notify on|off|list <EVENT>'") 311 action = args.pop(0) 312 events = args 313 314 if action == "on": 315 if not events: events = ('started','finished') 316 self.add_notification_events(events) 317 318 self.list_notified_events() 319 320 elif action == "off": 321 if events: 322 self.remove_notification_events(events) 323 else: 324 self.remove_all_notification_events() 325 326 self.list_notified_events() 327 328 elif action == "list": 329 self.list_notified_events() 330 return 331 332 else: 333 raise UsageError("try 'notify on|off <EVENT>'")
334 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)" 336
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" 363
364 - def builderAdded(self, builderName, builder):
365 if (self.bot.categories != None and 366 builder.category not in self.bot.categories): 367 return 368 369 log.msg('[Contact] Builder %s added' % (builder)) 370 builder.subscribe(self)
371
372 - def builderRemoved(self, builderName):
373 log.msg('[Contact] Builder %s removed' % (builderName))
374
375 - def buildStarted(self, builderName, build):
376 builder = build.getBuilder() 377 log.msg('[Contact] Builder %r in category %s started' % (builder, builder.category)) 378 379 # only notify about builders we are interested in 380 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 385 386 if not self.notify_for('started'): 387 return 388 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()]) 397 ) 398 399 self.send(r)
400 401 results_descriptions = { 402 SUCCESS: ("Success", 'GREEN'), 403 WARNINGS: ("Warnings", 'YELLOW'), 404 FAILURE: ("Failure", 'RED'), 405 EXCEPTION: ("Exception", 'PURPLE'), 406 RETRY: ("Retry", 'AQUA_LIGHT'), 407 } 408
409 - def getResultsDescriptionAndColor(self, results):
410 return self.results_descriptions.get(results, ("??",'RED'))
411
412 - def buildFinished(self, builderName, build, results):
413 builder = build.getBuilder() 414 415 if (self.bot.categories != None and 416 builder.category not in self.bot.categories): 417 return 418 419 if not self.notify_for_finished(build): 420 return 421 422 builder_name = builder.getName() 423 buildnum = build.getNumber() 424 buildrevs = build.getRevisions() 425 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]) 434 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 439 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]))) 442 443 self.send(r)
444
445 - def notify_for_finished(self, build):
446 results = build.getResults() 447 448 if self.notify_for('finished'): 449 return True 450 451 if self.notify_for(lower(self.results_descriptions.get(results)[0])): 452 return True 453 454 prevBuild = build.getPreviousBuild() 455 if prevBuild: 456 prevResult = prevBuild.getResults() 457 458 required_notification_control_string = join((lower(self.results_descriptions.get(prevResult)[0]), \ 459 'To', \ 460 capitalize(self.results_descriptions.get(results)[0])), \ 461 '') 462 463 if (self.notify_for(required_notification_control_string)): 464 return True 465 466 return False
467
468 - def watchedBuildFinished(self, b):
469 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 475 476 builder_name = builder.getName() 477 buildnum = b.getNumber() 478 buildrevs = b.getRevisions() 479 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]) 488 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)
494
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) 505 506 which = opts['builder'] 507 branch = opts['branch'] 508 revision = opts['revision'] 509 reason = opts['reason'] 510 props = opts['props'] 511 512 if which is None: 513 raise UsageError("you must provide a Builder, " + errReply) 514 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 528 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] 537 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") 549 550 bc = self.getControl(which) 551 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")
560 561 562 command_FORCE.usage = "force build [--branch=branch] [--revision=revision] [--props=prop1=val1,prop2=val2...] <which> <reason> - Force a build" 563
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] 570 571 buildercontrol = self.getControl(which) 572 573 r = "stopped: by %s: %s" % (self.describeUser(who), reason) 574 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() 584 585 # obtain the BuildControl object 586 buildcontrol = buildercontrol.getBuild(num) 587 588 # make it stop 589 buildcontrol.stopBuild(r) 590 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)
596 597 command_STOP.usage = "stop build <which> <reason> - Stop a running build" 598
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)
624
625 - def command_LAST(self, args, who):
626 args = shlex.split(args) 627 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>'" 634 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))
644 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>" 652
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
660
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
665 666 # commands 667
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" 673
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'" 682
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>" 699
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" 704
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" 710
711 - def command_DESTROY(self, args, who):
712 self.act("readies phasers")
713
714 - def command_DANCE(self, args, who):
715 reactor.callLater(1.0, self.send, "<(^.^<)") 716 reactor.callLater(2.0, self.send, "<(^.^)>") 717 reactor.callLater(3.0, self.send, "(>^.^)>") 718 reactor.callLater(3.5, self.send, "(7^.^)7") 719 reactor.callLater(5.0, self.send, "(>^.^<)")
720 721 # communication with the user 722
723 - def send(self, message):
724 if not self.muted: 725 self.bot.msgOrNotice(self.dest, message.encode("ascii", "replace"))
726
727 - def act(self, action):
728 if not self.muted: 729 self.bot.describe(self.dest, action.encode("ascii", "replace"))
730 731 # main dispatchers for incoming messages 732
733 - def getCommandMethod(self, command):
734 return getattr(self, 'command_' + command.upper(), None)
735
736 - def handleMessage(self, message, who):
737 # a message has arrived from 'who'. For broadcast contacts (i.e. when 738 # people do an irc 'buildbot: command'), this will be a string 739 # describing the sender of the message in some useful-to-log way, and 740 # a single Contact may see messages from a variety of users. For 741 # unicast contacts (i.e. when people do an irc '/msg buildbot 742 # command'), a single Contact will only ever see messages from a 743 # single user. 744 message = message.lstrip() 745 if self.silly.has_key(message): 746 self.doSilly(message) 747 return defer.succeed(None) 748 749 parts = message.split(' ', 1) 750 if len(parts) == 1: 751 parts = parts + [''] 752 cmd, args = parts 753 log.msg("irc command", cmd) 754 755 meth = self.getCommandMethod(cmd) 756 if not meth and message[-1] == '!': 757 self.send("What you say!") 758 return defer.succeed(None) 759 760 if meth: 761 d = defer.maybeDeferred(meth, args.strip(), who) 762 @d.addErrback 763 def usageError(f): 764 f.trap(UsageError) 765 self.send(str(f.value))
766 @d.addErrback 767 def logErr(f): 768 log.err(f) 769 self.send("Something bad happened (see logs)") 770 d.addErrback(log.err) 771 return d 772 return defer.succeed(None) 773
774 - def handleAction(self, data, user):
775 # this is sent when somebody performs an action that mentions the 776 # buildbot (like '/me kicks buildbot'). 'user' is the name/nick/id of 777 # the person who performed the action, so if their action provokes a 778 # response, they can be named. This is 100% silly. 779 if not data.endswith("s "+ self.bot.nickname): 780 return 781 words = data.split() 782 verb = words[-2] 783 if verb == "kicks": 784 response = "%s back" % verb 785 else: 786 response = "%s %s too" % (verb, user) 787 self.act(response)
788
789 790 -class IrcStatusBot(irc.IRCClient):
791 """I represent the buildbot to an IRC server. 792 """ 793 contactClass = IRCContact 794
795 - def __init__(self, nickname, password, channels, pm_to_nicks, status, 796 categories, notify_events, noticeOnChannel=False, 797 useRevisions=False, showBlameList=False, useColors=True):
798 self.nickname = nickname 799 self.channels = channels 800 self.pm_to_nicks = pm_to_nicks 801 self.password = password 802 self.status = status 803 self.master = status.master 804 self.categories = categories 805 self.notify_events = notify_events 806 self.hasQuit = 0 807 self.contacts = {} 808 self.noticeOnChannel = noticeOnChannel 809 self.useColors = useColors 810 self.useRevisions = useRevisions 811 self.showBlameList = showBlameList 812 self._keepAliveCall = task.LoopingCall(lambda: self.ping(self.nickname))
813
814 - def connectionMade(self):
815 irc.IRCClient.connectionMade(self) 816 self._keepAliveCall.start(60)
817
818 - def connectionLost(self, reason):
819 if self._keepAliveCall.running: 820 self._keepAliveCall.stop() 821 irc.IRCClient.connectionLost(self, reason)
822
823 - def msgOrNotice(self, dest, message):
824 if self.noticeOnChannel and dest[0] == '#': 825 self.notice(dest, message) 826 else: 827 self.msg(dest, message)
828
829 - def getContact(self, name):
830 name = name.lower() # nicknames and channel names are case insensitive 831 if name in self.contacts: 832 return self.contacts[name] 833 new_contact = self.contactClass(self, name) 834 self.contacts[name] = new_contact 835 return new_contact
836
837 - def log(self, msg):
838 log.msg("%s: %s" % (self, msg))
839 840 841 # the following irc.IRCClient methods are called when we have input 842
843 - def privmsg(self, user, channel, message):
844 user = user.split('!', 1)[0] # rest is ~user@hostname 845 # channel is '#twisted' or 'buildbot' (for private messages) 846 if channel == self.nickname: 847 # private message 848 contact = self.getContact(user) 849 contact.handleMessage(message, user) 850 return 851 # else it's a broadcast message, maybe for us, maybe not. 'channel' 852 # is '#twisted' or the like. 853 contact = self.getContact(channel) 854 if message.startswith("%s:" % self.nickname) or message.startswith("%s," % self.nickname): 855 message = message[len("%s:" % self.nickname):] 856 contact.handleMessage(message, user)
857
858 - def action(self, user, channel, data):
859 user = user.split('!', 1)[0] # rest is ~user@hostname 860 # somebody did an action (/me actions) in the broadcast channel 861 contact = self.getContact(channel) 862 if self.nickname in data: 863 contact.handleAction(data, user)
864
865 - def signedOn(self):
866 if self.password: 867 self.msg("Nickserv", "IDENTIFY " + self.password) 868 for c in self.channels: 869 if isinstance(c, dict): 870 channel = c.get('channel', None) 871 password = c.get('password', None) 872 else: 873 channel = c 874 password = None 875 self.join(channel=channel, key=password) 876 for c in self.pm_to_nicks: 877 self.getContact(c)
878
879 - def joined(self, channel):
880 self.log("I have joined %s" % (channel,)) 881 # trigger contact contructor, which in turn subscribes to notify events 882 self.getContact(channel)
883
884 - def left(self, channel):
885 self.log("I have left %s" % (channel,))
886
887 - def kickedFrom(self, channel, kicker, message):
888 self.log("I have been kicked from %s by %s: %s" % (channel, 889 kicker, 890 message))
891
892 893 -class ThrottledClientFactory(protocol.ClientFactory):
894 lostDelay = random.randint(1, 5) 895 failedDelay = random.randint(45, 60) 896
897 - def __init__(self, lostDelay=None, failedDelay=None):
898 if lostDelay is not None: 899 self.lostDelay = lostDelay 900 if failedDelay is not None: 901 self.failedDelay = failedDelay
902
903 - def clientConnectionLost(self, connector, reason):
904 reactor.callLater(self.lostDelay, connector.connect)
905
906 - def clientConnectionFailed(self, connector, reason):
907 reactor.callLater(self.failedDelay, connector.connect)
908
909 910 -class IrcStatusFactory(ThrottledClientFactory):
911 protocol = IrcStatusBot 912 913 status = None 914 control = None 915 shuttingDown = False 916 p = None 917
918 - def __init__(self, nickname, password, channels, pm_to_nicks, categories, notify_events, 919 noticeOnChannel=False, useRevisions=False, showBlameList=False, 920 lostDelay=None, failedDelay=None, useColors=True):
921 ThrottledClientFactory.__init__(self, lostDelay=lostDelay, 922 failedDelay=failedDelay) 923 self.status = None 924 self.nickname = nickname 925 self.password = password 926 self.channels = channels 927 self.pm_to_nicks = pm_to_nicks 928 self.categories = categories 929 self.notify_events = notify_events 930 self.noticeOnChannel = noticeOnChannel 931 self.useRevisions = useRevisions 932 self.showBlameList = showBlameList 933 self.useColors = useColors
934
935 - def __getstate__(self):
936 d = self.__dict__.copy() 937 del d['p'] 938 return d
939
940 - def shutdown(self):
941 self.shuttingDown = True 942 if self.p: 943 self.p.quit("buildmaster reconfigured: bot disconnecting")
944
945 - def buildProtocol(self, address):
946 p = self.protocol(self.nickname, self.password, 947 self.channels, self.pm_to_nicks, self.status, 948 self.categories, self.notify_events, 949 noticeOnChannel = self.noticeOnChannel, 950 useColors = self.useColors, 951 useRevisions = self.useRevisions, 952 showBlameList = self.showBlameList) 953 p.factory = self 954 p.status = self.status 955 p.control = self.control 956 self.p = p 957 return p
958 959 # TODO: I think a shutdown that occurs while the connection is being 960 # established will make this explode 961
962 - def clientConnectionLost(self, connector, reason):
963 if self.shuttingDown: 964 log.msg("not scheduling reconnection attempt") 965 return 966 ThrottledClientFactory.clientConnectionLost(self, connector, reason)
967
968 - def clientConnectionFailed(self, connector, reason):
969 if self.shuttingDown: 970 log.msg("not scheduling reconnection attempt") 971 return 972 ThrottledClientFactory.clientConnectionFailed(self, connector, reason)
973
974 975 -class IRC(base.StatusReceiverMultiService):
976 implements(IStatusReceiver) 977 978 in_test_harness = False 979 980 compare_attrs = ["host", "port", "nick", "password", 981 "channels", "pm_to_nicks", "allowForce", "useSSL", 982 "useRevisions", "categories", "useColors", 983 "lostDelay", "failedDelay"] 984
985 - def __init__(self, host, nick, channels, pm_to_nicks=[], port=6667, 986 allowForce=False, categories=None, password=None, notify_events={}, 987 noticeOnChannel = False, showBlameList = True, useRevisions=False, 988 useSSL=False, lostDelay=None, failedDelay=None, useColors=True):
989 base.StatusReceiverMultiService.__init__(self) 990 991 assert allowForce in (True, False) # TODO: implement others 992 993 # need to stash these so we can detect changes later 994 self.host = host 995 self.port = port 996 self.nick = nick 997 self.channels = channels 998 self.pm_to_nicks = pm_to_nicks 999 self.password = password 1000 self.allowForce = allowForce 1001 self.useRevisions = useRevisions 1002 self.categories = categories 1003 self.notify_events = notify_events 1004 1005 self.f = IrcStatusFactory(self.nick, self.password, 1006 self.channels, self.pm_to_nicks, 1007 self.categories, self.notify_events, 1008 noticeOnChannel = noticeOnChannel, 1009 useRevisions = useRevisions, 1010 showBlameList = showBlameList, 1011 lostDelay = lostDelay, 1012 failedDelay = failedDelay, 1013 useColors = useColors) 1014 1015 if useSSL: 1016 # SSL client needs a ClientContextFactory for some SSL mumbo-jumbo 1017 if not have_ssl: 1018 raise RuntimeError("useSSL requires PyOpenSSL") 1019 cf = ssl.ClientContextFactory() 1020 c = internet.SSLClient(self.host, self.port, self.f, cf) 1021 else: 1022 c = internet.TCPClient(self.host, self.port, self.f) 1023 1024 c.setServiceParent(self)
1025
1026 - def setServiceParent(self, parent):
1027 base.StatusReceiverMultiService.setServiceParent(self, parent) 1028 self.f.status = parent 1029 if self.allowForce: 1030 self.f.control = interfaces.IControl(self.master)
1031
1032 - def stopService(self):
1033 # make sure the factory will stop reconnecting 1034 self.f.shutdown() 1035 return base.StatusReceiverMultiService.stopService(self)
1036