OLD | NEW |
| (Empty) |
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.cate
gories) | |
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_e
vents.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 | |
185 def subscribe_to_build_events(self): | |
186 self.channel.status.subscribe(self) | |
187 self.subscribed = 1 | |
188 | |
189 def unsubscribe_from_build_events(self): | |
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 | |
209 def remove_all_notification_events(self): | |
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 b
uild events. event should be one or more of: 'started', 'finished', 'failure',
'success', 'exception' or 'xToY' (where x and Y are one of success, warnings, fa
ilure, 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, builde
r.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 disable
d') | |
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.ge
tChanges())) + "]" | |
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, build
er.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_desc
riptions.get(prevResult)), \ | |
361 'To', \ | |
362 capitalize(self.res
ults_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 <whi
ch>" | |
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_ev
ents, | |
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 | |
OLD | NEW |