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

Source Code for Module buildbot.status.words

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