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