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