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