remove a lot of unused imports, marked by pyflakes
[buildbot.git] / buildbot / status / html.py
blob731a7c4acd35ee068368ee92f9e243d0bc868f5a
1 # -*- test-case-name: buildbot.test.test_web -*-
3 from __future__ import generators
5 from twisted.python import log, components
6 import urllib, re
8 from twisted.internet import defer, reactor
9 from twisted.web.resource import Resource
10 from twisted.web import static, html, server, distrib
11 from twisted.web.error import NoResource
12 from twisted.web.util import Redirect, DeferredResource
13 from twisted.application import strports
14 from twisted.spread import pb
16 from buildbot.twcompat import implements, Interface
18 import sys, string, types, time, os.path
20 from buildbot import interfaces, util
21 from buildbot import version
22 from buildbot.sourcestamp import SourceStamp
23 from buildbot.status import builder, base
24 from buildbot.changes import changes
25 from buildbot.process.base import BuildRequest
27 class ITopBox(Interface):
28 """I represent a box in the top row of the waterfall display: the one
29 which shows the status of the last build for each builder."""
30 pass
32 class ICurrentBox(Interface):
33 """I represent the 'current activity' box, just above the builder name."""
34 pass
36 class IBox(Interface):
37 """I represent a box in the waterfall display."""
38 pass
40 class IHTMLLog(Interface):
41 pass
43 ROW_TEMPLATE = '''
44 <div class="row">
45 <span class="label">%(label)s</span>
46 <span class="field">%(field)s</span>
47 </div>'''
49 def make_row(label, field):
50 """Create a name/value row for the HTML.
52 `label` is plain text; it will be HTML-encoded.
54 `field` is a bit of HTML structure; it will not be encoded in
55 any way.
56 """
57 label = html.escape(label)
58 return ROW_TEMPLATE % {"label": label, "field": field}
60 colormap = {
61 'green': '#72ff75',
63 def td(text="", parms={}, **props):
64 data = ""
65 data += " "
66 #if not props.has_key("border"):
67 # props["border"] = 1
68 props.update(parms)
69 if props.has_key("bgcolor"):
70 props["bgcolor"] = colormap.get(props["bgcolor"], props["bgcolor"])
71 comment = props.get("comment", None)
72 if comment:
73 data += "<!-- %s -->" % comment
74 data += "<td"
75 class_ = props.get('class_', None)
76 if class_:
77 props["class"] = class_
78 for prop in ("align", "bgcolor", "colspan", "rowspan", "border",
79 "valign", "halign", "class"):
80 p = props.get(prop, None)
81 if p != None:
82 data += " %s=\"%s\"" % (prop, p)
83 data += ">"
84 if not text:
85 text = "&nbsp;"
86 if type(text) == types.ListType:
87 data += string.join(text, "<br />")
88 else:
89 data += text
90 data += "</td>\n"
91 return data
93 def build_get_class(b):
94 """
95 Return the class to use for a finished build or buildstep,
96 based on the result.
97 """
98 # FIXME: this getResults duplicity might need to be fixed
99 result = b.getResults()
100 #print "THOMAS: result for b %r: %r" % (b, result)
101 if isinstance(b, builder.BuildStatus):
102 result = b.getResults()
103 elif isinstance(b, builder.BuildStepStatus):
104 result = b.getResults()[0]
105 # after forcing a build, b.getResults() returns ((None, []), []), ugh
106 if isinstance(result, tuple):
107 result = result[0]
108 else:
109 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
111 if result == None:
112 # FIXME: this happens when a buildstep is running ?
113 return "running"
114 return builder.Results[result]
116 class Box:
117 # a Box wraps an Event. The Box has HTML <td> parameters that Events
118 # lack, and it has a base URL to which each File's name is relative.
119 # Events don't know about HTML.
120 spacer = False
121 def __init__(self, text=[], color=None, class_=None, urlbase=None,
122 **parms):
123 self.text = text
124 self.color = color
125 self.class_ = class_
126 self.urlbase = urlbase
127 self.show_idle = 0
128 if parms.has_key('show_idle'):
129 del parms['show_idle']
130 self.show_idle = 1
132 self.parms = parms
133 # parms is a dict of HTML parameters for the <td> element that will
134 # represent this Event in the waterfall display.
136 def td(self, **props):
137 props.update(self.parms)
138 text = self.text
139 if not text and self.show_idle:
140 text = ["[idle]"]
141 return td(text, props, bgcolor=self.color, class_=self.class_)
144 class HtmlResource(Resource):
145 css = None
146 contentType = "text/html; charset=UTF-8"
147 def render(self, request):
148 data = self.content(request)
149 request.setHeader("content-type", self.contentType)
150 if request.method == "HEAD":
151 request.setHeader("content-length", len(data))
152 return ''
153 return data
154 title = "Dummy"
155 def content(self, request):
156 data = ('<!DOCTYPE html PUBLIC'
157 ' "-//W3C//DTD XHTML 1.0 Transitional//EN"\n'
158 '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
159 '<html'
160 ' xmlns="http://www.w3.org/1999/xhtml"'
161 ' lang="en"'
162 ' xml:lang="en">\n')
163 data += "<head>\n"
164 data += " <title>" + self.title + "</title>\n"
165 if self.css:
166 # TODO: use some sort of relative link up to the root page, so
167 # this css can be used from child pages too
168 data += (' <link href="%s" rel="stylesheet" type="text/css"/>\n'
169 % "buildbot.css")
170 data += "</head>\n"
171 data += '<body vlink="#800080">\n'
172 data += self.body(request)
173 data += "</body></html>\n"
174 return data
175 def body(self, request):
176 return "Dummy\n"
178 class StaticHTML(HtmlResource):
179 def __init__(self, body, title):
180 HtmlResource.__init__(self)
181 self.bodyHTML = body
182 self.title = title
183 def body(self, request):
184 return self.bodyHTML
186 # $builder/builds/NN/stepname
187 class StatusResourceBuildStep(HtmlResource):
188 title = "Build Step"
190 def __init__(self, status, step):
191 HtmlResource.__init__(self)
192 self.status = status
193 self.step = step
195 def body(self, request):
196 s = self.step
197 b = s.getBuild()
198 data = "<h1>BuildStep %s:#%d:%s</h1>\n" % \
199 (b.getBuilder().getName(), b.getNumber(), s.getName())
201 if s.isFinished():
202 data += ("<h2>Finished</h2>\n"
203 "<p>%s</p>\n" % html.escape("%s" % s.getText()))
204 else:
205 data += ("<h2>Not Finished</h2>\n"
206 "<p>ETA %s seconds</p>\n" % s.getETA())
208 exp = s.getExpectations()
209 if exp:
210 data += ("<h2>Expectations</h2>\n"
211 "<ul>\n")
212 for e in exp:
213 data += "<li>%s: current=%s, target=%s</li>\n" % \
214 (html.escape(e[0]), e[1], e[2])
215 data += "</ul>\n"
216 logs = s.getLogs()
217 if logs:
218 data += ("<h2>Logs</h2>\n"
219 "<ul>\n")
220 for num in range(len(logs)):
221 if logs[num].hasContents():
222 # FIXME: If the step name has a / in it, this is broken
223 # either way. If we quote it but say '/'s are safe,
224 # it chops up the step name. If we quote it and '/'s
225 # are not safe, it escapes the / that separates the
226 # step name from the log number.
227 data += '<li><a href="%s">%s</a></li>\n' % \
228 (urllib.quote(request.childLink("%d" % num)),
229 html.escape(logs[num].getName()))
230 else:
231 data += ('<li>%s</li>\n' %
232 html.escape(logs[num].getName()))
233 data += "</ul>\n"
235 return data
237 def getChild(self, path, request):
238 logname = path
239 try:
240 log = self.step.getLogs()[int(logname)]
241 if log.hasContents():
242 return IHTMLLog(interfaces.IStatusLog(log))
243 return NoResource("Empty Log '%s'" % logname)
244 except (IndexError, ValueError):
245 return NoResource("No such Log '%s'" % logname)
247 # $builder/builds/NN/tests/TESTNAME
248 class StatusResourceTestResult(HtmlResource):
249 title = "Test Logs"
251 def __init__(self, status, name, result):
252 HtmlResource.__init__(self)
253 self.status = status
254 self.name = name
255 self.result = result
257 def body(self, request):
258 dotname = ".".join(self.name)
259 logs = self.result.getLogs()
260 lognames = logs.keys()
261 lognames.sort()
262 data = "<h1>%s</h1>\n" % html.escape(dotname)
263 for name in lognames:
264 data += "<h2>%s</h2>\n" % html.escape(name)
265 data += "<pre>" + logs[name] + "</pre>\n\n"
267 return data
270 # $builder/builds/NN/tests
271 class StatusResourceTestResults(HtmlResource):
272 title = "Test Results"
274 def __init__(self, status, results):
275 HtmlResource.__init__(self)
276 self.status = status
277 self.results = results
279 def body(self, request):
280 r = self.results
281 data = "<h1>Test Results</h1>\n"
282 data += "<ul>\n"
283 testnames = r.keys()
284 testnames.sort()
285 for name in testnames:
286 res = r[name]
287 dotname = ".".join(name)
288 data += " <li>%s: " % dotname
289 # TODO: this could break on weird test names. At the moment,
290 # test names only come from Trial tests, where the name
291 # components must be legal python names, but that won't always
292 # be a restriction.
293 url = request.childLink(dotname)
294 data += "<a href=\"%s\">%s</a>" % (url, " ".join(res.getText()))
295 data += "</li>\n"
296 data += "</ul>\n"
297 return data
299 def getChild(self, path, request):
300 try:
301 name = tuple(path.split("."))
302 result = self.results[name]
303 return StatusResourceTestResult(self.status, name, result)
304 except KeyError:
305 return NoResource("No such test name '%s'" % path)
308 # $builder/builds/NN
309 class StatusResourceBuild(HtmlResource):
310 title = "Build"
312 def __init__(self, status, build, builderControl, buildControl):
313 HtmlResource.__init__(self)
314 self.status = status
315 self.build = build
316 self.builderControl = builderControl
317 self.control = buildControl
319 def body(self, request):
320 b = self.build
321 buildbotURL = self.status.getBuildbotURL()
322 projectName = self.status.getProjectName()
323 data = '<div class="title"><a href="%s">%s</a></div>\n'%(buildbotURL,
324 projectName)
325 # the color in the following line gives python-mode trouble
326 data += ("<h1>Build <a href=\"%s\">%s</a>:#%d</h1>\n"
327 "<h2>Reason:</h2>\n%s\n"
328 % (self.status.getURLForThing(b.getBuilder()),
329 b.getBuilder().getName(), b.getNumber(),
330 html.escape(b.getReason())))
332 branch, revision, patch = b.getSourceStamp()
333 data += "<h2>SourceStamp:</h2>\n"
334 data += " <ul>\n"
335 if branch:
336 data += " <li>Branch: %s</li>\n" % html.escape(branch)
337 if revision:
338 data += " <li>Revision: %s</li>\n" % html.escape(str(revision))
339 if patch:
340 data += " <li>Patch: YES</li>\n" # TODO: provide link to .diff
341 if b.getChanges():
342 data += " <li>Changes: see below</li>\n"
343 if (branch is None and revision is None and patch is None
344 and not b.getChanges()):
345 data += " <li>build of most recent revision</li>\n"
346 data += " </ul>\n"
347 if b.isFinished():
348 data += "<h4>Buildslave: %s</h4>\n" % html.escape(b.getSlavename())
349 data += "<h2>Results:</h2>\n"
350 data += " ".join(b.getText()) + "\n"
351 if b.getTestResults():
352 url = request.childLink("tests")
353 data += "<h3><a href=\"%s\">test results</a></h3>\n" % url
354 else:
355 data += "<h2>Build In Progress</h2>"
356 if self.control is not None:
357 stopURL = urllib.quote(request.childLink("stop"))
358 data += """
359 <form action="%s" class='command stopbuild'>
360 <p>To stop this build, fill out the following fields and
361 push the 'Stop' button</p>\n""" % stopURL
362 data += make_row("Your name:",
363 "<input type='text' name='username' />")
364 data += make_row("Reason for stopping build:",
365 "<input type='text' name='comments' />")
366 data += """<input type="submit" value="Stop Builder" />
367 </form>
370 if b.isFinished() and self.builderControl is not None:
371 data += "<h3>Resubmit Build:</h3>\n"
372 # can we rebuild it exactly?
373 exactly = (revision is not None) or b.getChanges()
374 if exactly:
375 data += ("<p>This tree was built from a specific set of \n"
376 "source files, and can be rebuilt exactly</p>\n")
377 else:
378 data += ("<p>This tree was built from the most recent "
379 "revision")
380 if branch:
381 data += " (along some branch)"
382 data += (" and thus it might not be possible to rebuild it \n"
383 "exactly. Any changes that have been committed \n"
384 "after this build was started <b>will</b> be \n"
385 "included in a rebuild.</p>\n")
386 rebuildURL = urllib.quote(request.childLink("rebuild"))
387 data += ('<form action="%s" class="command rebuild">\n'
388 % rebuildURL)
389 data += make_row("Your name:",
390 "<input type='text' name='username' />")
391 data += make_row("Reason for re-running build:",
392 "<input type='text' name='comments' />")
393 data += '<input type="submit" value="Rebuild" />\n'
395 data += "<h2>Steps and Logfiles:</h2>\n"
396 if b.getLogs():
397 data += "<ol>\n"
398 for s in b.getSteps():
399 data += (" <li><a href=\"%s\">%s</a> [%s]\n"
400 % (self.status.getURLForThing(s), s.getName(),
401 " ".join(s.getText())))
402 if s.getLogs():
403 data += " <ol>\n"
404 for logfile in s.getLogs():
405 data += (" <li><a href=\"%s\">%s</a></li>\n" %
406 (self.status.getURLForThing(logfile),
407 logfile.getName()))
408 data += " </ol>\n"
409 data += " </li>\n"
410 data += "</ol>\n"
412 data += ("<h2>Blamelist:</h2>\n"
413 " <ol>\n")
414 for who in b.getResponsibleUsers():
415 data += " <li>%s</li>\n" % html.escape(who)
416 data += (" </ol>\n"
417 "<h2>All Changes</h2>\n")
418 changes = b.getChanges()
419 if changes:
420 data += "<ol>\n"
421 for c in changes:
422 data += "<li>" + c.asHTML() + "</li>\n"
423 data += "</ol>\n"
424 #data += html.PRE(b.changesText()) # TODO
425 return data
427 def stop(self, request):
428 log.msg("web stopBuild of build %s:%s" % \
429 (self.build.getBuilder().getName(),
430 self.build.getNumber()))
431 name = request.args.get("username", ["<unknown>"])[0]
432 comments = request.args.get("comments", ["<no reason specified>"])[0]
433 reason = ("The web-page 'stop build' button was pressed by "
434 "'%s': %s\n" % (name, comments))
435 self.control.stopBuild(reason)
436 # we're at http://localhost:8080/svn-hello/builds/5/stop?[args] and
437 # we want to go to: http://localhost:8080/svn-hello/builds/5 or
438 # http://localhost:8080/
440 #return Redirect("../%d" % self.build.getNumber())
441 r = Redirect("../../..")
442 d = defer.Deferred()
443 reactor.callLater(1, d.callback, r)
444 return DeferredResource(d)
446 def rebuild(self, request):
447 log.msg("web rebuild of build %s:%s" % \
448 (self.build.getBuilder().getName(),
449 self.build.getNumber()))
450 name = request.args.get("username", ["<unknown>"])[0]
451 comments = request.args.get("comments", ["<no reason specified>"])[0]
452 reason = ("The web-page 'rebuild' button was pressed by "
453 "'%s': %s\n" % (name, comments))
454 if not self.builderControl or not self.build.isFinished():
455 log.msg("could not rebuild: bc=%s, isFinished=%s"
456 % (self.builderControl, self.build.isFinished()))
457 # TODO: indicate an error
458 else:
459 self.builderControl.resubmitBuild(self.build, reason)
460 # we're at http://localhost:8080/svn-hello/builds/5/rebuild?[args] and
461 # we want to go to the top, at http://localhost:8080/
462 r = Redirect("../../..")
463 d = defer.Deferred()
464 reactor.callLater(1, d.callback, r)
465 return DeferredResource(d)
467 def getChild(self, path, request):
468 if path == "tests":
469 return StatusResourceTestResults(self.status,
470 self.build.getTestResults())
471 if path == "stop":
472 return self.stop(request)
473 if path == "rebuild":
474 return self.rebuild(request)
475 if path.startswith("step-"):
476 stepname = path[len("step-"):]
477 steps = self.build.getSteps()
478 for s in steps:
479 if s.getName() == stepname:
480 return StatusResourceBuildStep(self.status, s)
481 return NoResource("No such BuildStep '%s'" % stepname)
482 return NoResource("No such resource '%s'" % path)
484 # $builder
485 class StatusResourceBuilder(HtmlResource):
487 def __init__(self, status, builder, control):
488 HtmlResource.__init__(self)
489 self.status = status
490 self.title = builder.getName() + " Builder"
491 self.builder = builder
492 self.control = control
494 def body(self, request):
495 b = self.builder
496 slaves = b.getSlaves()
497 connected_slaves = [s for s in slaves if s.isConnected()]
499 buildbotURL = self.status.getBuildbotURL()
500 projectName = self.status.getProjectName()
501 data = "<a href=\"%s\">%s</a>\n" % (buildbotURL, projectName)
502 data += make_row("Builder:", html.escape(b.getName()))
503 b1 = b.getBuild(-1)
504 if b1 is not None:
505 data += make_row("Current/last build:", str(b1.getNumber()))
506 data += "\n<br />BUILDSLAVES<br />\n"
507 data += "<ol>\n"
508 for slave in slaves:
509 data += "<li><b>%s</b>: " % html.escape(slave.getName())
510 if slave.isConnected():
511 data += "CONNECTED\n"
512 if slave.getAdmin():
513 data += make_row("Admin:", html.escape(slave.getAdmin()))
514 if slave.getHost():
515 data += "<span class='label'>Host info:</span>\n"
516 data += html.PRE(slave.getHost())
517 else:
518 data += ("NOT CONNECTED\n")
519 data += "</li>\n"
520 data += "</ol>\n"
522 if self.control is not None and connected_slaves:
523 forceURL = urllib.quote(request.childLink("force"))
524 data += (
526 <form action='%(forceURL)s' class='command forcebuild'>
527 <p>To force a build, fill out the following fields and
528 push the 'Force Build' button</p>"""
529 + make_row("Your name:",
530 "<input type='text' name='username' />")
531 + make_row("Reason for build:",
532 "<input type='text' name='comments' />")
533 + make_row("Branch to build:",
534 "<input type='text' name='branch' />")
535 + make_row("Revision to build:",
536 "<input type='text' name='revision' />")
537 + """
538 <input type='submit' value='Force Build' />
539 </form>
540 """) % {"forceURL": forceURL}
541 elif self.control is not None:
542 data += """
543 <p>All buildslaves appear to be offline, so it's not possible
544 to force this build to execute at this time.</p>
547 if self.control is not None:
548 pingURL = urllib.quote(request.childLink("ping"))
549 data += """
550 <form action="%s" class='command pingbuilder'>
551 <p>To ping the buildslave(s), push the 'Ping' button</p>
553 <input type="submit" value="Ping Builder" />
554 </form>
555 """ % pingURL
557 return data
559 def force(self, request):
560 name = request.args.get("username", ["<unknown>"])[0]
561 reason = request.args.get("comments", ["<no reason specified>"])[0]
562 branch = request.args.get("branch", [""])[0]
563 revision = request.args.get("revision", [""])[0]
565 r = "The web-page 'force build' button was pressed by '%s': %s\n" \
566 % (name, reason)
567 log.msg("web forcebuild of builder '%s', branch='%s', revision='%s'"
568 % (self.builder.name, branch, revision))
570 if not self.control:
571 # TODO: tell the web user that their request was denied
572 log.msg("but builder control is disabled")
573 return Redirect("..")
575 # keep weird stuff out of the branch and revision strings. TODO:
576 # centralize this somewhere.
577 if not re.match(r'^[\w\.\-\/]*$', branch):
578 log.msg("bad branch '%s'" % branch)
579 return Redirect("..")
580 if not re.match(r'^[\w\.\-\/]*$', revision):
581 log.msg("bad revision '%s'" % revision)
582 return Redirect("..")
583 if branch == "":
584 branch = None
585 if revision == "":
586 revision = None
588 # TODO: if we can authenticate that a particular User pushed the
589 # button, use their name instead of None, so they'll be informed of
590 # the results.
591 s = SourceStamp(branch=branch, revision=revision)
592 req = BuildRequest(r, s, self.builder.getName())
593 try:
594 self.control.requestBuildSoon(req)
595 except interfaces.NoSlaveError:
596 # TODO: tell the web user that their request could not be
597 # honored
598 pass
599 return Redirect("..")
601 def ping(self, request):
602 log.msg("web ping of builder '%s'" % self.builder.name)
603 self.control.ping() # TODO: there ought to be an ISlaveControl
604 return Redirect("..")
606 def getChild(self, path, request):
607 if path == "force":
608 return self.force(request)
609 if path == "ping":
610 return self.ping(request)
611 if not path in ("events", "builds"):
612 return NoResource("Bad URL '%s'" % path)
613 num = request.postpath.pop(0)
614 request.prepath.append(num)
615 num = int(num)
616 if path == "events":
617 # TODO: is this dead code? .statusbag doesn't exist,right?
618 log.msg("getChild['path']: %s" % request.uri)
619 return NoResource("events are unavailable until code gets fixed")
620 filename = request.postpath.pop(0)
621 request.prepath.append(filename)
622 e = self.builder.statusbag.getEventNumbered(num)
623 if not e:
624 return NoResource("No such event '%d'" % num)
625 file = e.files.get(filename, None)
626 if file == None:
627 return NoResource("No such file '%s'" % filename)
628 if type(file) == type(""):
629 if file[:6] in ("<HTML>", "<html>"):
630 return static.Data(file, "text/html")
631 return static.Data(file, "text/plain")
632 return file
633 if path == "builds":
634 build = self.builder.getBuild(num)
635 if build:
636 control = None
637 if self.control:
638 control = self.control.getBuild(num)
639 return StatusResourceBuild(self.status, build,
640 self.control, control)
641 else:
642 return NoResource("No such build '%d'" % num)
643 return NoResource("really weird URL %s" % path)
645 # $changes/NN
646 class StatusResourceChanges(HtmlResource):
647 def __init__(self, status, changemaster):
648 HtmlResource.__init__(self)
649 self.status = status
650 self.changemaster = changemaster
651 def body(self, request):
652 data = ""
653 data += "Change sources:\n"
654 sources = list(self.changemaster)
655 if sources:
656 data += "<ol>\n"
657 for s in sources:
658 data += "<li>%s</li>\n" % s.describe()
659 data += "</ol>\n"
660 else:
661 data += "none (push only)\n"
662 return data
663 def getChild(self, path, request):
664 num = int(path)
665 c = self.changemaster.getChangeNumbered(num)
666 if not c:
667 return NoResource("No change number '%d'" % num)
668 return StaticHTML(c.asHTML(), "Change #%d" % num)
670 textlog_stylesheet = """
671 <style type="text/css">
672 div.data {
673 font-family: "Courier New", courier, monotype;
675 span.stdout {
676 font-family: "Courier New", courier, monotype;
678 span.stderr {
679 font-family: "Courier New", courier, monotype;
680 color: red;
682 span.header {
683 font-family: "Courier New", courier, monotype;
684 color: blue;
686 </style>
689 class ChunkConsumer:
690 if implements:
691 implements(interfaces.IStatusLogConsumer)
692 else:
693 __implements__ = interfaces.IStatusLogConsumer,
695 def __init__(self, original, textlog):
696 self.original = original
697 self.textlog = textlog
698 def registerProducer(self, producer, streaming):
699 self.producer = producer
700 self.original.registerProducer(producer, streaming)
701 def unregisterProducer(self):
702 self.original.unregisterProducer()
703 def writeChunk(self, chunk):
704 formatted = self.textlog.content([chunk])
705 try:
706 self.original.write(formatted)
707 except pb.DeadReferenceError:
708 self.producing.stopProducing()
709 def finish(self):
710 self.textlog.finished()
712 class TextLog(Resource):
713 # a new instance of this Resource is created for each client who views
714 # it, so we can afford to track the request in the Resource.
715 if implements:
716 implements(IHTMLLog)
717 else:
718 __implements__ = IHTMLLog,
720 asText = False
721 subscribed = False
723 def __init__(self, original):
724 Resource.__init__(self)
725 self.original = original
727 def getChild(self, path, request):
728 if path == "text":
729 self.asText = True
730 return self
731 return NoResource("bad pathname")
733 def htmlHeader(self, request):
734 title = "Log File contents"
735 data = "<html>\n<head><title>" + title + "</title>\n"
736 data += textlog_stylesheet
737 data += "</head>\n"
738 data += "<body vlink=\"#800080\">\n"
739 texturl = request.childLink("text")
740 data += '<a href="%s">(view as text)</a><br />\n' % texturl
741 data += "<pre>\n"
742 return data
744 def content(self, entries):
745 spanfmt = '<span class="%s">%s</span>'
746 data = ""
747 for type, entry in entries:
748 if self.asText:
749 if type != builder.HEADER:
750 data += entry
751 else:
752 data += spanfmt % (builder.ChunkTypes[type],
753 html.escape(entry))
754 return data
756 def htmlFooter(self):
757 data = "</pre>\n"
758 data += "</body></html>\n"
759 return data
761 def render_HEAD(self, request):
762 if self.asText:
763 request.setHeader("content-type", "text/plain")
764 else:
765 request.setHeader("content-type", "text/html")
767 # vague approximation, ignores markup
768 request.setHeader("content-length", self.original.length)
769 return ''
771 def render_GET(self, req):
772 self.req = req
774 if self.asText:
775 req.setHeader("content-type", "text/plain")
776 else:
777 req.setHeader("content-type", "text/html")
779 if not self.asText:
780 req.write(self.htmlHeader(req))
782 self.original.subscribeConsumer(ChunkConsumer(req, self))
783 return server.NOT_DONE_YET
785 def finished(self):
786 if not self.req:
787 return
788 try:
789 if not self.asText:
790 self.req.write(self.htmlFooter())
791 self.req.finish()
792 except pb.DeadReferenceError:
793 pass
794 # break the cycle, the Request's .notifications list includes the
795 # Deferred (from req.notifyFinish) that's pointing at us.
796 self.req = None
798 components.registerAdapter(TextLog, interfaces.IStatusLog, IHTMLLog)
801 class HTMLLog(Resource):
802 if implements:
803 implements(IHTMLLog)
804 else:
805 __implements__ = IHTMLLog,
808 def __init__(self, original):
809 Resource.__init__(self)
810 self.original = original
812 def render(self, request):
813 request.setHeader("content-type", "text/html")
814 return self.original.html
816 components.registerAdapter(HTMLLog, builder.HTMLLogFile, IHTMLLog)
819 class CurrentBox(components.Adapter):
820 # this provides the "current activity" box, just above the builder name
821 if implements:
822 implements(ICurrentBox)
823 else:
824 __implements__ = ICurrentBox,
826 def formatETA(self, eta):
827 if eta is None:
828 return []
829 if eta < 0:
830 return ["Soon"]
831 abstime = time.strftime("%H:%M:%S", time.localtime(util.now()+eta))
832 return ["ETA in", "%d secs" % eta, "at %s" % abstime]
834 def getBox(self, status):
835 # getState() returns offline, idle, or building
836 state, builds = self.original.getState()
838 # look for upcoming builds. We say the state is "waiting" if the
839 # builder is otherwise idle and there is a scheduler which tells us a
840 # build will be performed some time in the near future. TODO: this
841 # functionality used to be in BuilderStatus.. maybe this code should
842 # be merged back into it.
843 upcoming = []
844 builderName = self.original.getName()
845 for s in status.getSchedulers():
846 if builderName in s.listBuilderNames():
847 upcoming.extend(s.getPendingBuildTimes())
848 if state == "idle" and upcoming:
849 state = "waiting"
851 if state == "building":
852 color = "yellow"
853 text = ["building"]
854 if builds:
855 for b in builds:
856 eta = b.getETA()
857 if eta:
858 text.extend(self.formatETA(eta))
859 elif state == "offline":
860 color = "red"
861 text = ["offline"]
862 elif state == "idle":
863 color = "white"
864 text = ["idle"]
865 elif state == "waiting":
866 color = "yellow"
867 text = ["waiting"]
868 else:
869 # just in case I add a state and forget to update this
870 color = "white"
871 text = [state]
873 # TODO: for now, this pending/upcoming stuff is in the "current
874 # activity" box, but really it should go into a "next activity" row
875 # instead. The only times it should show up in "current activity" is
876 # when the builder is otherwise idle.
878 # are any builds pending? (waiting for a slave to be free)
879 pbs = self.original.getPendingBuilds()
880 if pbs:
881 text.append("%d pending" % len(pbs))
882 for t in upcoming:
883 text.extend(["next at",
884 time.strftime("%H:%M:%S", time.localtime(t)),
885 "[%d secs]" % (t - util.now()),
887 # TODO: the upcoming-builds box looks like:
888 # ['waiting', 'next at', '22:14:15', '[86 secs]']
889 # while the currently-building box is reversed:
890 # ['building', 'ETA in', '2 secs', 'at 22:12:50']
891 # consider swapping one of these to make them look the same. also
892 # consider leaving them reversed to make them look different.
893 return Box(text, color=color, class_="Activity " + state)
895 components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
897 class ChangeBox(components.Adapter):
898 if implements:
899 implements(IBox)
900 else:
901 __implements__ = IBox,
903 def getBox(self):
904 url = "changes/%d" % self.original.number
905 text = '<a href="%s">%s</a>' % (url, html.escape(self.original.who))
906 return Box([text], color="white", class_="Change")
907 components.registerAdapter(ChangeBox, changes.Change, IBox)
909 class BuildBox(components.Adapter):
910 # this provides the yellow "starting line" box for each build
911 if implements:
912 implements(IBox)
913 else:
914 __implements__ = IBox,
916 def getBox(self):
917 b = self.original
918 name = b.getBuilder().getName()
919 number = b.getNumber()
920 url = "%s/builds/%d" % (urllib.quote(name, safe=''), number)
921 reason = b.getReason()
922 text = ('<a title="Reason: %s" href="%s">Build %d</a>'
923 % (html.escape(reason), url, number))
924 color = "yellow"
925 class_ = "start"
926 if b.isFinished() and not b.getSteps():
927 # the steps have been pruned, so there won't be any indication
928 # of whether it succeeded or failed. Color the box red or green
929 # to show its status
930 color = b.getColor()
931 class_ = build_get_class(b)
932 return Box([text], color=color, class_="BuildStep " + class_)
933 components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
935 class StepBox(components.Adapter):
936 if implements:
937 implements(IBox)
938 else:
939 __implements__ = IBox,
941 def getBox(self):
942 b = self.original.getBuild()
943 urlbase = "%s/builds/%d/step-%s" % (
944 urllib.quote(b.getBuilder().getName(), safe=''),
945 b.getNumber(),
946 urllib.quote(self.original.getName(), safe=''))
947 text = self.original.getText()
948 if text is None:
949 log.msg("getText() gave None", urlbase)
950 text = []
951 text = text[:]
952 logs = self.original.getLogs()
953 for num in range(len(logs)):
954 name = logs[num].getName()
955 if logs[num].hasContents():
956 url = "%s/%d" % (urlbase, num)
957 text.append("<a href=\"%s\">%s</a>" % (url, html.escape(name)))
958 else:
959 text.append(html.escape(name))
960 urls = self.original.getURLs()
961 ex_url_class = "BuildStep external"
962 for name, target in urls.items():
963 text.append('[<a href="%s" class="%s">%s</a>]' %
964 (target, ex_url_class, html.escape(name)))
965 color = self.original.getColor()
966 class_ = "BuildStep " + build_get_class(self.original)
967 return Box(text, color, class_=class_)
968 components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
970 class EventBox(components.Adapter):
971 if implements:
972 implements(IBox)
973 else:
974 __implements__ = IBox,
976 def getBox(self):
977 text = self.original.getText()
978 color = self.original.getColor()
979 class_ = "Event"
980 if color:
981 class_ += " " + color
982 return Box(text, color, class_=class_)
983 components.registerAdapter(EventBox, builder.Event, IBox)
986 class BuildTopBox(components.Adapter):
987 # this provides a per-builder box at the very top of the display,
988 # showing the results of the most recent build
989 if implements:
990 implements(IBox)
991 else:
992 __implements__ = IBox,
994 def getBox(self):
995 assert interfaces.IBuilderStatus(self.original)
996 b = self.original.getLastFinishedBuild()
997 if not b:
998 return Box(["none"], "white", class_="LastBuild")
999 name = b.getBuilder().getName()
1000 number = b.getNumber()
1001 url = "%s/builds/%d" % (name, number)
1002 text = b.getText()
1003 # TODO: add logs?
1004 # TODO: add link to the per-build page at 'url'
1005 c = b.getColor()
1006 class_ = build_get_class(b)
1007 return Box(text, c, class_="LastBuild %s" % class_)
1008 components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
1010 class Spacer(builder.Event):
1011 def __init__(self, start, finish):
1012 self.started = start
1013 self.finished = finish
1015 class SpacerBox(components.Adapter):
1016 if implements:
1017 implements(IBox)
1018 else:
1019 __implements__ = IBox,
1021 def getBox(self):
1022 #b = Box(["spacer"], "white")
1023 b = Box([])
1024 b.spacer = True
1025 return b
1026 components.registerAdapter(SpacerBox, Spacer, IBox)
1028 def insertGaps(g, lastEventTime, idleGap=2):
1029 debug = False
1031 e = g.next()
1032 starts, finishes = e.getTimes()
1033 if debug: log.msg("E0", starts, finishes)
1034 if finishes == 0:
1035 finishes = starts
1036 if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \
1037 (finishes, idleGap, lastEventTime))
1038 if finishes is not None and finishes + idleGap < lastEventTime:
1039 if debug: log.msg(" spacer0")
1040 yield Spacer(finishes, lastEventTime)
1042 followingEventStarts = starts
1043 if debug: log.msg(" fES0", starts)
1044 yield e
1046 while 1:
1047 e = g.next()
1048 starts, finishes = e.getTimes()
1049 if debug: log.msg("E2", starts, finishes)
1050 if finishes == 0:
1051 finishes = starts
1052 if finishes is not None and finishes + idleGap < followingEventStarts:
1053 # there is a gap between the end of this event and the beginning
1054 # of the next one. Insert an idle event so the waterfall display
1055 # shows a gap here.
1056 if debug:
1057 log.msg(" finishes=%s, gap=%s, fES=%s" % \
1058 (finishes, idleGap, followingEventStarts))
1059 yield Spacer(finishes, followingEventStarts)
1060 yield e
1061 followingEventStarts = starts
1062 if debug: log.msg(" fES1", starts)
1065 class WaterfallStatusResource(HtmlResource):
1066 """This builds the main status page, with the waterfall display, and
1067 all child pages."""
1068 title = "BuildBot"
1069 def __init__(self, status, changemaster, categories, css=None):
1070 HtmlResource.__init__(self)
1071 self.status = status
1072 self.changemaster = changemaster
1073 self.categories = categories
1074 p = self.status.getProjectName()
1075 if p:
1076 self.title = "BuildBot: %s" % p
1077 self.css = css
1079 def body(self, request):
1080 "This method builds the main waterfall display."
1082 data = ''
1084 projectName = self.status.getProjectName()
1085 projectURL = self.status.getProjectURL()
1087 phase = request.args.get("phase",["2"])
1088 phase = int(phase[0])
1090 showBuilders = request.args.get("show", None)
1091 allBuilders = self.status.getBuilderNames(categories=self.categories)
1092 if showBuilders:
1093 builderNames = []
1094 for b in showBuilders:
1095 if b not in allBuilders:
1096 continue
1097 if b in builderNames:
1098 continue
1099 builderNames.append(b)
1100 else:
1101 builderNames = allBuilders
1102 builders = map(lambda name: self.status.getBuilder(name),
1103 builderNames)
1105 if phase == -1:
1106 return self.body0(request, builders)
1107 (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
1108 self.buildGrid(request, builders)
1109 if phase == 0:
1110 return self.phase0(request, (changeNames + builderNames),
1111 timestamps, eventGrid)
1112 # start the table: top-header material
1113 data += '<table border="0" cellspacing="0">\n'
1115 if projectName and projectURL:
1116 # TODO: this is going to look really ugly
1117 topleft = "<a href=\"%s\">%s</a><br />last build" % \
1118 (projectURL, projectName)
1119 else:
1120 topleft = "last build"
1121 data += ' <tr class="LastBuild">\n'
1122 data += td(topleft, align="right", colspan=2, class_="Project")
1123 for b in builders:
1124 box = ITopBox(b).getBox()
1125 data += box.td(align="center")
1126 data += " </tr>\n"
1128 data += ' <tr class="Activity">\n'
1129 data += td('current activity', align='right', colspan=2)
1130 for b in builders:
1131 box = ICurrentBox(b).getBox(self.status)
1132 data += box.td(align="center")
1133 data += " </tr>\n"
1135 data += " <tr>\n"
1136 TZ = time.tzname[time.daylight]
1137 data += td("time (%s)" % TZ, align="center", class_="Time")
1138 name = changeNames[0]
1139 data += td(
1140 "<a href=\"%s\">%s</a>" % (urllib.quote(name, safe=''), name),
1141 align="center", class_="Change")
1142 for name in builderNames:
1143 data += td(
1144 #"<a href=\"%s\">%s</a>" % (request.childLink(name), name),
1145 "<a href=\"%s\">%s</a>" % (urllib.quote(name, safe=''), name),
1146 align="center", class_="Builder")
1147 data += " </tr>\n"
1149 if phase == 1:
1150 f = self.phase1
1151 else:
1152 f = self.phase2
1153 data += f(request, changeNames + builderNames, timestamps, eventGrid,
1154 sourceEvents)
1156 data += "</table>\n"
1158 data += "<hr />\n"
1160 data += "<a href=\"http://buildbot.sourceforge.net/\">Buildbot</a>"
1161 data += "-%s " % version
1162 if projectName:
1163 data += "working for the "
1164 if projectURL:
1165 data += "<a href=\"%s\">%s</a> project." % (projectURL,
1166 projectName)
1167 else:
1168 data += "%s project." % projectName
1169 data += "<br />\n"
1170 # TODO: push this to the right edge, if possible
1171 data += ("Page built: " +
1172 time.strftime("%a %d %b %Y %H:%M:%S",
1173 time.localtime(util.now()))
1174 + "\n")
1175 return data
1177 def body0(self, request, builders):
1178 # build the waterfall display
1179 data = ""
1180 data += "<h2>Basic display</h2>\n"
1181 data += "<p>See <a href=\"%s\">here</a>" % \
1182 urllib.quote(request.childLink("waterfall"))
1183 data += " for the waterfall display</p>\n"
1185 data += '<table border="0" cellspacing="0">\n'
1186 names = map(lambda builder: builder.name, builders)
1188 # the top row is two blank spaces, then the top-level status boxes
1189 data += " <tr>\n"
1190 data += td("", colspan=2)
1191 for b in builders:
1192 text = ""
1193 color = "#ca88f7"
1194 state, builds = b.getState()
1195 if state != "offline":
1196 text += "%s<br />\n" % state #b.getCurrentBig().text[0]
1197 else:
1198 text += "OFFLINE<br />\n"
1199 color = "#ffe0e0"
1200 data += td(text, align="center", bgcolor=color)
1202 # the next row has the column headers: time, changes, builder names
1203 data += " <tr>\n"
1204 data += td("Time", align="center")
1205 data += td("Changes", align="center")
1206 for name in names:
1207 data += td(
1208 "<a href=\"%s\">%s</a>" % (urllib.quote(request.childLink(name)), name),
1209 align="center")
1210 data += " </tr>\n"
1212 # all further rows involve timestamps, commit events, and build events
1213 data += " <tr>\n"
1214 data += td("04:00", align="bottom")
1215 data += td("fred", align="center")
1216 for name in names:
1217 data += td("stuff", align="center", bgcolor="red")
1218 data += " </tr>\n"
1220 data += "</table>\n"
1221 return data
1223 def buildGrid(self, request, builders):
1224 debug = False
1226 # XXX: see if we can use a cached copy
1228 # first step is to walk backwards in time, asking each column
1229 # (commit, all builders) if they have any events there. Build up the
1230 # array of events, and stop when we have a reasonable number.
1232 commit_source = self.changemaster
1234 lastEventTime = util.now()
1235 sources = [commit_source] + builders
1236 changeNames = ["changes"]
1237 builderNames = map(lambda builder: builder.getName(), builders)
1238 sourceNames = changeNames + builderNames
1239 sourceEvents = []
1240 sourceGenerators = []
1241 for s in sources:
1242 gen = insertGaps(s.eventGenerator(), lastEventTime)
1243 sourceGenerators.append(gen)
1244 # get the first event
1245 try:
1246 e = gen.next()
1247 event = interfaces.IStatusEvent(e)
1248 if debug:
1249 log.msg("gen %s gave1 %s" % (gen, event.getText()))
1250 except StopIteration:
1251 event = None
1252 sourceEvents.append(event)
1253 eventGrid = []
1254 timestamps = []
1255 spanLength = 10 # ten-second chunks
1256 tooOld = util.now() - 12*60*60 # never show more than 12 hours
1257 maxPageLen = 200
1259 lastEventTime = 0
1260 for e in sourceEvents:
1261 if e and e.getTimes()[0] > lastEventTime:
1262 lastEventTime = e.getTimes()[0]
1263 if lastEventTime == 0:
1264 lastEventTime = util.now()
1266 spanStart = lastEventTime - spanLength
1267 debugGather = 0
1269 while 1:
1270 if debugGather: log.msg("checking (%s,]" % spanStart)
1271 # the tableau of potential events is in sourceEvents[]. The
1272 # window crawls backwards, and we examine one source at a time.
1273 # If the source's top-most event is in the window, is it pushed
1274 # onto the events[] array and the tableau is refilled. This
1275 # continues until the tableau event is not in the window (or is
1276 # missing).
1278 spanEvents = [] # for all sources, in this span. row of eventGrid
1279 firstTimestamp = None # timestamp of first event in the span
1280 lastTimestamp = None # last pre-span event, for next span
1282 for c in range(len(sourceGenerators)):
1283 events = [] # for this source, in this span. cell of eventGrid
1284 event = sourceEvents[c]
1285 while event and spanStart < event.getTimes()[0]:
1286 # to look at windows that don't end with the present,
1287 # condition the .append on event.time <= spanFinish
1288 if not IBox(event, None):
1289 log.msg("BAD EVENT", event, event.getText())
1290 assert 0
1291 if debug:
1292 log.msg("pushing", event.getText(), event)
1293 events.append(event)
1294 starts, finishes = event.getTimes()
1295 firstTimestamp = util.earlier(firstTimestamp, starts)
1296 try:
1297 event = sourceGenerators[c].next()
1298 #event = interfaces.IStatusEvent(event)
1299 if debug:
1300 log.msg("gen[%s] gave2 %s" % (sourceNames[c],
1301 event.getText()))
1302 except StopIteration:
1303 event = None
1304 if debug:
1305 log.msg("finished span")
1307 if event:
1308 # this is the last pre-span event for this source
1309 lastTimestamp = util.later(lastTimestamp,
1310 event.getTimes()[0])
1311 if debugGather:
1312 log.msg(" got %s from %s" % (events, sourceNames[c]))
1313 sourceEvents[c] = event # refill the tableau
1314 spanEvents.append(events)
1316 if firstTimestamp is not None:
1317 eventGrid.append(spanEvents)
1318 timestamps.append(firstTimestamp)
1321 if lastTimestamp:
1322 spanStart = lastTimestamp - spanLength
1323 else:
1324 # no more events
1325 break
1326 if lastTimestamp < tooOld:
1327 pass
1328 #break
1329 if len(timestamps) > maxPageLen:
1330 break
1333 # now loop
1335 # loop is finished. now we have eventGrid[] and timestamps[]
1336 if debugGather: log.msg("finished loop")
1337 assert(len(timestamps) == len(eventGrid))
1338 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
1340 def phase0(self, request, sourceNames, timestamps, eventGrid):
1341 # phase0 rendering
1342 if not timestamps:
1343 return "no events"
1344 data = ""
1345 for r in range(0, len(timestamps)):
1346 data += "<p>\n"
1347 data += "[%s]<br />" % timestamps[r]
1348 row = eventGrid[r]
1349 assert(len(row) == len(sourceNames))
1350 for c in range(0, len(row)):
1351 if row[c]:
1352 data += "<b>%s</b><br />\n" % sourceNames[c]
1353 for e in row[c]:
1354 log.msg("Event", r, c, sourceNames[c], e.getText())
1355 lognames = [loog.getName() for loog in e.getLogs()]
1356 data += "%s: %s: %s %s<br />" % (e.getText(),
1357 e.getTimes()[0],
1358 e.getColor(),
1359 lognames)
1360 else:
1361 data += "<b>%s</b> [none]<br />\n" % sourceNames[c]
1362 return data
1364 def phase1(self, request, sourceNames, timestamps, eventGrid,
1365 sourceEvents):
1366 # phase1 rendering: table, but boxes do not overlap
1367 data = ""
1368 if not timestamps:
1369 return data
1370 lastDate = None
1371 for r in range(0, len(timestamps)):
1372 chunkstrip = eventGrid[r]
1373 # chunkstrip is a horizontal strip of event blocks. Each block
1374 # is a vertical list of events, all for the same source.
1375 assert(len(chunkstrip) == len(sourceNames))
1376 maxRows = reduce(lambda x,y: max(x,y),
1377 map(lambda x: len(x), chunkstrip))
1378 for i in range(maxRows):
1379 data += " <tr>\n";
1380 if i == 0:
1381 stuff = []
1382 # add the date at the beginning, and each time it changes
1383 today = time.strftime("<b>%d %b %Y</b>",
1384 time.localtime(timestamps[r]))
1385 todayday = time.strftime("<b>%a</b>",
1386 time.localtime(timestamps[r]))
1387 if today != lastDate:
1388 stuff.append(todayday)
1389 stuff.append(today)
1390 lastDate = today
1391 stuff.append(
1392 time.strftime("%H:%M:%S",
1393 time.localtime(timestamps[r])))
1394 data += td(stuff, valign="bottom", align="center",
1395 rowspan=maxRows, class_="Time")
1396 for c in range(0, len(chunkstrip)):
1397 block = chunkstrip[c]
1398 assert(block != None) # should be [] instead
1399 # bottom-justify
1400 offset = maxRows - len(block)
1401 if i < offset:
1402 data += td("")
1403 else:
1404 e = block[i-offset]
1405 box = IBox(e).getBox()
1406 box.parms["show_idle"] = 1
1407 data += box.td(valign="top", align="center")
1408 data += " </tr>\n"
1410 return data
1412 def phase2(self, request, sourceNames, timestamps, eventGrid,
1413 sourceEvents):
1414 data = ""
1415 if not timestamps:
1416 return data
1417 # first pass: figure out the height of the chunks, populate grid
1418 grid = []
1419 for i in range(1+len(sourceNames)):
1420 grid.append([])
1421 # grid is a list of columns, one for the timestamps, and one per
1422 # event source. Each column is exactly the same height. Each element
1423 # of the list is a single <td> box.
1424 lastDate = time.strftime("<b>%d %b %Y</b>",
1425 time.localtime(util.now()))
1426 for r in range(0, len(timestamps)):
1427 chunkstrip = eventGrid[r]
1428 # chunkstrip is a horizontal strip of event blocks. Each block
1429 # is a vertical list of events, all for the same source.
1430 assert(len(chunkstrip) == len(sourceNames))
1431 maxRows = reduce(lambda x,y: max(x,y),
1432 map(lambda x: len(x), chunkstrip))
1433 for i in range(maxRows):
1434 if i != maxRows-1:
1435 grid[0].append(None)
1436 else:
1437 # timestamp goes at the bottom of the chunk
1438 stuff = []
1439 # add the date at the beginning (if it is not the same as
1440 # today's date), and each time it changes
1441 todayday = time.strftime("<b>%a</b>",
1442 time.localtime(timestamps[r]))
1443 today = time.strftime("<b>%d %b %Y</b>",
1444 time.localtime(timestamps[r]))
1445 if today != lastDate:
1446 stuff.append(todayday)
1447 stuff.append(today)
1448 lastDate = today
1449 stuff.append(
1450 time.strftime("%H:%M:%S",
1451 time.localtime(timestamps[r])))
1452 grid[0].append(Box(text=stuff, class_="Time",
1453 valign="bottom", align="center"))
1455 # at this point the timestamp column has been populated with
1456 # maxRows boxes, most None but the last one has the time string
1457 for c in range(0, len(chunkstrip)):
1458 block = chunkstrip[c]
1459 assert(block != None) # should be [] instead
1460 for i in range(maxRows - len(block)):
1461 # fill top of chunk with blank space
1462 grid[c+1].append(None)
1463 for i in range(len(block)):
1464 # so the events are bottom-justified
1465 b = IBox(block[i]).getBox()
1466 b.parms['valign'] = "top"
1467 b.parms['align'] = "center"
1468 grid[c+1].append(b)
1469 # now all the other columns have maxRows new boxes too
1470 # populate the last row, if empty
1471 gridlen = len(grid[0])
1472 for i in range(len(grid)):
1473 strip = grid[i]
1474 assert(len(strip) == gridlen)
1475 if strip[-1] == None:
1476 if sourceEvents[i-1]:
1477 filler = IBox(sourceEvents[i-1]).getBox()
1478 else:
1479 # this can happen if you delete part of the build history
1480 filler = Box(text=["?"], align="center")
1481 strip[-1] = filler
1482 strip[-1].parms['rowspan'] = 1
1483 # second pass: bubble the events upwards to un-occupied locations
1484 # Every square of the grid that has a None in it needs to have
1485 # something else take its place.
1486 noBubble = request.args.get("nobubble",['0'])
1487 noBubble = int(noBubble[0])
1488 if not noBubble:
1489 for col in range(len(grid)):
1490 strip = grid[col]
1491 if col == 1: # changes are handled differently
1492 for i in range(2, len(strip)+1):
1493 # only merge empty boxes. Don't bubble commit boxes.
1494 if strip[-i] == None:
1495 next = strip[-i+1]
1496 assert(next)
1497 if next:
1498 #if not next.event:
1499 if next.spacer:
1500 # bubble the empty box up
1501 strip[-i] = next
1502 strip[-i].parms['rowspan'] += 1
1503 strip[-i+1] = None
1504 else:
1505 # we are above a commit box. Leave it
1506 # be, and turn the current box into an
1507 # empty one
1508 strip[-i] = Box([], rowspan=1,
1509 comment="commit bubble")
1510 strip[-i].spacer = True
1511 else:
1512 # we are above another empty box, which
1513 # somehow wasn't already converted.
1514 # Shouldn't happen
1515 pass
1516 else:
1517 for i in range(2, len(strip)+1):
1518 # strip[-i] will go from next-to-last back to first
1519 if strip[-i] == None:
1520 # bubble previous item up
1521 assert(strip[-i+1] != None)
1522 strip[-i] = strip[-i+1]
1523 strip[-i].parms['rowspan'] += 1
1524 strip[-i+1] = None
1525 else:
1526 strip[-i].parms['rowspan'] = 1
1527 # third pass: render the HTML table
1528 for i in range(gridlen):
1529 data += " <tr>\n";
1530 for strip in grid:
1531 b = strip[i]
1532 if b:
1533 data += b.td()
1534 else:
1535 if noBubble:
1536 data += td([])
1537 # Nones are left empty, rowspan should make it all fit
1538 data += " </tr>\n"
1539 return data
1542 class StatusResource(Resource):
1543 status = None
1544 control = None
1545 favicon = None
1546 robots_txt = None
1548 def __init__(self, status, control, changemaster, categories, css):
1550 @type status: L{buildbot.status.builder.Status}
1551 @type control: L{buildbot.master.Control}
1552 @type changemaster: L{buildbot.changes.changes.ChangeMaster}
1554 Resource.__init__(self)
1555 self.status = status
1556 self.control = control
1557 self.changemaster = changemaster
1558 self.categories = categories
1559 self.css = css
1560 waterfall = WaterfallStatusResource(self.status, changemaster,
1561 categories, css)
1562 self.putChild("", waterfall)
1564 def render(self, request):
1565 request.redirect(request.prePathURL() + '/')
1566 request.finish()
1568 def getChild(self, path, request):
1569 if path == "robots.txt" and self.robots_txt:
1570 return static.File(self.robots_txt)
1571 if path == "buildbot.css" and self.css:
1572 return static.File(self.css)
1573 if path == "changes":
1574 return StatusResourceChanges(self.status, self.changemaster)
1575 if path == "favicon.ico":
1576 if self.favicon:
1577 return static.File(self.favicon)
1578 return NoResource("No favicon.ico registered")
1580 if path in self.status.getBuilderNames():
1581 builder = self.status.getBuilder(path)
1582 control = None
1583 if self.control:
1584 control = self.control.getBuilder(path)
1585 return StatusResourceBuilder(self.status, builder, control)
1587 return NoResource("No such Builder '%s'" % path)
1589 if hasattr(sys, "frozen"):
1590 # all 'data' files are in the directory of our executable
1591 here = os.path.dirname(sys.executable)
1592 buildbot_icon = os.path.abspath(os.path.join(here, "buildbot.png"))
1593 buildbot_css = os.path.abspath(os.path.join(here, "classic.css"))
1594 else:
1595 # running from source
1596 # the icon is sibpath(__file__, "../buildbot.png") . This is for
1597 # portability.
1598 up = os.path.dirname
1599 buildbot_icon = os.path.abspath(os.path.join(up(up(__file__)),
1600 "buildbot.png"))
1601 buildbot_css = os.path.abspath(os.path.join(up(__file__), "classic.css"))
1603 class Waterfall(base.StatusReceiverMultiService):
1604 """I implement the primary web-page status interface, called a 'Waterfall
1605 Display' because builds and steps are presented in a grid of boxes which
1606 move downwards over time. The top edge is always the present. Each column
1607 represents a single builder. Each box describes a single Step, which may
1608 have logfiles or other status information.
1610 All these pages are served via a web server of some sort. The simplest
1611 approach is to let the buildmaster run its own webserver, on a given TCP
1612 port, but it can also publish its pages to a L{twisted.web.distrib}
1613 distributed web server (which lets the buildbot pages be a subset of some
1614 other web server).
1616 Since 0.6.3, BuildBot defines class attributes on elements so they can be
1617 styled with CSS stylesheets. Buildbot uses some generic classes to
1618 identify the type of object, and some more specific classes for the
1619 various kinds of those types. It does this by specifying both in the
1620 class attributes where applicable, separated by a space. It is important
1621 that in your CSS you declare the more generic class styles above the more
1622 specific ones. For example, first define a style for .Event, and below
1623 that for .SUCCESS
1625 The following CSS class names are used:
1626 - Activity, Event, BuildStep, LastBuild: general classes
1627 - waiting, interlocked, building, offline, idle: Activity states
1628 - start, running, success, failure, warnings, skipped, exception:
1629 LastBuild and BuildStep states
1630 - Change: box with change
1631 - Builder: box for builder name (at top)
1632 - Project
1633 - Time
1635 @type parent: L{buildbot.master.BuildMaster}
1636 @ivar parent: like all status plugins, this object is a child of the
1637 BuildMaster, so C{.parent} points to a
1638 L{buildbot.master.BuildMaster} instance, through which
1639 the status-reporting object is acquired.
1642 compare_attrs = ["http_port", "distrib_port", "allowForce",
1643 "categories", "css", "favicon", "robots_txt"]
1645 def __init__(self, http_port=None, distrib_port=None, allowForce=True,
1646 categories=None, css=buildbot_css, favicon=buildbot_icon,
1647 robots_txt=None):
1648 """To have the buildbot run its own web server, pass a port number to
1649 C{http_port}. To have it run a web.distrib server
1651 @type http_port: int or L{twisted.application.strports} string
1652 @param http_port: a strports specification describing which port the
1653 buildbot should use for its web server, with the
1654 Waterfall display as the root page. For backwards
1655 compatibility this can also be an int. Use
1656 'tcp:8000' to listen on that port, or
1657 'tcp:12345:interface=127.0.0.1' if you only want
1658 local processes to connect to it (perhaps because
1659 you are using an HTTP reverse proxy to make the
1660 buildbot available to the outside world, and do not
1661 want to make the raw port visible).
1663 @type distrib_port: int or L{twisted.application.strports} string
1664 @param distrib_port: Use this if you want to publish the Waterfall
1665 page using web.distrib instead. The most common
1666 case is to provide a string that is an absolute
1667 pathname to the unix socket on which the
1668 publisher should listen
1669 (C{os.path.expanduser(~/.twistd-web-pb)} will
1670 match the default settings of a standard
1671 twisted.web 'personal web server'). Another
1672 possibility is to pass an integer, which means
1673 the publisher should listen on a TCP socket,
1674 allowing the web server to be on a different
1675 machine entirely. Both forms are provided for
1676 backwards compatibility; the preferred form is a
1677 strports specification like
1678 'unix:/home/buildbot/.twistd-web-pb'. Providing
1679 a non-absolute pathname will probably confuse
1680 the strports parser.
1682 @type allowForce: bool
1683 @param allowForce: if True, present a 'Force Build' button on the
1684 per-Builder page that allows visitors to the web
1685 site to initiate a build. If False, don't provide
1686 this button.
1688 @type favicon: string
1689 @param favicon: if set, provide the pathname of an image file that
1690 will be used for the 'favicon.ico' resource. Many
1691 browsers automatically request this file and use it
1692 as an icon in any bookmark generated from this site.
1693 Defaults to the buildbot/buildbot.png image provided
1694 in the distribution. Can be set to None to avoid
1695 using a favicon at all.
1697 @type robots_txt: string
1698 @param robots_txt: if set, provide the pathname of a robots.txt file.
1699 Many search engines request this file and obey the
1700 rules in it. E.g. to disallow them to crawl the
1701 status page, put the following two lines in
1702 robots.txt::
1703 User-agent: *
1704 Disallow: /
1707 base.StatusReceiverMultiService.__init__(self)
1708 assert allowForce in (True, False) # TODO: implement others
1709 if type(http_port) is int:
1710 http_port = "tcp:%d" % http_port
1711 self.http_port = http_port
1712 if distrib_port is not None:
1713 if type(distrib_port) is int:
1714 distrib_port = "tcp:%d" % distrib_port
1715 if distrib_port[0] in "/~.": # pathnames
1716 distrib_port = "unix:%s" % distrib_port
1717 self.distrib_port = distrib_port
1718 self.allowForce = allowForce
1719 self.categories = categories
1720 self.css = css
1721 self.favicon = favicon
1722 self.robots_txt = robots_txt
1724 def __repr__(self):
1725 if self.http_port is None:
1726 return "<Waterfall on path %s>" % self.distrib_port
1727 if self.distrib_port is None:
1728 return "<Waterfall on port %s>" % self.http_port
1729 return "<Waterfall on port %s and path %s>" % (self.http_port,
1730 self.distrib_port)
1732 def setServiceParent(self, parent):
1734 @type parent: L{buildbot.master.BuildMaster}
1736 base.StatusReceiverMultiService.setServiceParent(self, parent)
1737 self.setup()
1739 def setup(self):
1740 status = self.parent.getStatus()
1741 if self.allowForce:
1742 control = interfaces.IControl(self.parent)
1743 else:
1744 control = None
1745 change_svc = self.parent.change_svc
1746 sr = StatusResource(status, control, change_svc, self.categories,
1747 self.css)
1748 sr.favicon = self.favicon
1749 sr.robots_txt = self.robots_txt
1750 self.site = server.Site(sr)
1752 if self.http_port is not None:
1753 s = strports.service(self.http_port, self.site)
1754 s.setServiceParent(self)
1755 if self.distrib_port is not None:
1756 f = pb.PBServerFactory(distrib.ResourcePublisher(self.site))
1757 s = strports.service(self.distrib_port, f)
1758 s.setServiceParent(self)