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