1 # -*- test-case-name: buildbot.test.test_web -*-
3 from __future__
import generators
5 from twisted
.python
import log
, components
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."""
32 class ICurrentBox(Interface
):
33 """I represent the 'current activity' box, just above the builder name."""
36 class IBox(Interface
):
37 """I represent a box in the waterfall display."""
40 class IHTMLLog(Interface
):
45 <span class="label">%(label)s</span>
46 <span class="field">%(field)s</span>
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
57 label
= html
.escape(label
)
58 return ROW_TEMPLATE
% {"label": label
, "field": field
}
63 def td(text
="", parms
={}, **props
):
66 #if not props.has_key("border"):
69 if props
.has_key("bgcolor"):
70 props
["bgcolor"] = colormap
.get(props
["bgcolor"], props
["bgcolor"])
71 comment
= props
.get("comment", None)
73 data
+= "<!-- %s -->" % comment
75 class_
= props
.get('class_', None)
77 props
["class"] = class_
78 for prop
in ("align", "bgcolor", "colspan", "rowspan", "border",
79 "valign", "halign", "class"):
80 p
= props
.get(prop
, None)
82 data
+= " %s=\"%s\"" % (prop
, p
)
86 if type(text
) == types
.ListType
:
87 data
+= string
.join(text
, "<br />")
93 def build_get_class(b
):
95 Return the class to use for a finished build or buildstep,
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):
109 raise TypeError, "%r is not a BuildStatus or BuildStepStatus" % b
112 # FIXME: this happens when a buildstep is running ?
114 return builder
.Results
[result
]
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.
121 def __init__(self
, text
=[], color
=None, class_
=None, urlbase
=None,
126 self
.urlbase
= urlbase
128 if parms
.has_key('show_idle'):
129 del parms
['show_idle']
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
)
139 if not text
and self
.show_idle
:
141 return td(text
, props
, bgcolor
=self
.color
, class_
=self
.class_
)
144 class HtmlResource(Resource
):
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
))
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'
160 ' xmlns="http://www.w3.org/1999/xhtml"'
164 data
+= " <title>" + self
.title
+ "</title>\n"
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'
171 data
+= '<body vlink="#800080">\n'
172 data
+= self
.body(request
)
173 data
+= "</body></html>\n"
175 def body(self
, request
):
178 class StaticHTML(HtmlResource
):
179 def __init__(self
, body
, title
):
180 HtmlResource
.__init
__(self
)
183 def body(self
, request
):
186 # $builder/builds/NN/stepname
187 class StatusResourceBuildStep(HtmlResource
):
190 def __init__(self
, status
, step
):
191 HtmlResource
.__init
__(self
)
195 def body(self
, request
):
198 data
= "<h1>BuildStep %s:#%d:%s</h1>\n" % \
199 (b
.getBuilder().getName(), b
.getNumber(), s
.getName())
202 data
+= ("<h2>Finished</h2>\n"
203 "<p>%s</p>\n" % html
.escape("%s" % s
.getText()))
205 data
+= ("<h2>Not Finished</h2>\n"
206 "<p>ETA %s seconds</p>\n" % s
.getETA())
208 exp
= s
.getExpectations()
210 data
+= ("<h2>Expectations</h2>\n"
213 data
+= "<li>%s: current=%s, target=%s</li>\n" % \
214 (html
.escape(e
[0]), e
[1], e
[2])
218 data
+= ("<h2>Logs</h2>\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()))
231 data
+= ('<li>%s</li>\n' %
232 html
.escape(logs
[num
].getName()))
237 def getChild(self
, path
, request
):
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
):
251 def __init__(self
, status
, name
, result
):
252 HtmlResource
.__init
__(self
)
257 def body(self
, request
):
258 dotname
= ".".join(self
.name
)
259 logs
= self
.result
.getLogs()
260 lognames
= logs
.keys()
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"
270 # $builder/builds/NN/tests
271 class StatusResourceTestResults(HtmlResource
):
272 title
= "Test Results"
274 def __init__(self
, status
, results
):
275 HtmlResource
.__init
__(self
)
277 self
.results
= results
279 def body(self
, request
):
281 data
= "<h1>Test Results</h1>\n"
285 for name
in testnames
:
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
293 url
= request
.childLink(dotname
)
294 data
+= "<a href=\"%s\">%s</a>" % (url
, " ".join(res
.getText()))
299 def getChild(self
, path
, request
):
301 name
= tuple(path
.split("."))
302 result
= self
.results
[name
]
303 return StatusResourceTestResult(self
.status
, name
, result
)
305 return NoResource("No such test name '%s'" % path
)
309 class StatusResourceBuild(HtmlResource
):
312 def __init__(self
, status
, build
, builderControl
, buildControl
):
313 HtmlResource
.__init
__(self
)
316 self
.builderControl
= builderControl
317 self
.control
= buildControl
319 def body(self
, request
):
321 buildbotURL
= self
.status
.getBuildbotURL()
322 projectName
= self
.status
.getProjectName()
323 data
= '<div class="title"><a href="%s">%s</a></div>\n'%(buildbotURL
,
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"
336 data
+= " <li>Branch: %s</li>\n" % html
.escape(branch
)
338 data
+= " <li>Revision: %s</li>\n" % html
.escape(str(revision
))
340 data
+= " <li>Patch: YES</li>\n" # TODO: provide link to .diff
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"
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
355 data
+= "<h2>Build In Progress</h2>"
356 if self
.control
is not None:
357 stopURL
= urllib
.quote(request
.childLink("stop"))
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" />
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()
375 data
+= ("<p>This tree was built from a specific set of \n"
376 "source files, and can be rebuilt exactly</p>\n")
378 data
+= ("<p>This tree was built from the most recent "
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'
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"
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())))
404 for logfile
in s
.getLogs():
405 data
+= (" <li><a href=\"%s\">%s</a></li>\n" %
406 (self
.status
.getURLForThing(logfile
),
412 data
+= ("<h2>Blamelist:</h2>\n"
414 for who
in b
.getResponsibleUsers():
415 data
+= " <li>%s</li>\n" % html
.escape(who
)
417 "<h2>All Changes</h2>\n")
418 changes
= b
.getChanges()
422 data
+= "<li>" + c
.asHTML() + "</li>\n"
424 #data += html.PRE(b.changesText()) # TODO
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("../../..")
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
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("../../..")
464 reactor
.callLater(1, d
.callback
, r
)
465 return DeferredResource(d
)
467 def getChild(self
, path
, request
):
469 return StatusResourceTestResults(self
.status
,
470 self
.build
.getTestResults())
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()
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
)
485 class StatusResourceBuilder(HtmlResource
):
487 def __init__(self
, status
, builder
, control
):
488 HtmlResource
.__init
__(self
)
490 self
.title
= builder
.getName() + " Builder"
491 self
.builder
= builder
492 self
.control
= control
494 def body(self
, request
):
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()))
505 data
+= make_row("Current/last build:", str(b1
.getNumber()))
506 data
+= "\n<br />BUILDSLAVES<br />\n"
509 data
+= "<li><b>%s</b>: " % html
.escape(slave
.getName())
510 if slave
.isConnected():
511 data
+= "CONNECTED\n"
513 data
+= make_row("Admin:", html
.escape(slave
.getAdmin()))
515 data
+= "<span class='label'>Host info:</span>\n"
516 data
+= html
.PRE(slave
.getHost())
518 data
+= ("NOT CONNECTED\n")
522 if self
.control
is not None and connected_slaves
:
523 forceURL
= urllib
.quote(request
.childLink("force"))
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' />")
538 <input type='submit' value='Force Build' />
540 """) % {"forceURL": forceURL
}
541 elif self
.control
is not None:
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"))
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" />
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" \
567 log
.msg("web forcebuild of builder '%s', branch='%s', revision='%s'"
568 % (self
.builder
.name
, branch
, revision
))
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("..")
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
591 s
= SourceStamp(branch
=branch
, revision
=revision
)
592 req
= BuildRequest(r
, s
, self
.builder
.getName())
594 self
.control
.requestBuildSoon(req
)
595 except interfaces
.NoSlaveError
:
596 # TODO: tell the web user that their request could not be
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
):
608 return self
.force(request
)
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
)
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
)
624 return NoResource("No such event '%d'" % num
)
625 file = e
.files
.get(filename
, 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")
634 build
= self
.builder
.getBuild(num
)
638 control
= self
.control
.getBuild(num
)
639 return StatusResourceBuild(self
.status
, build
,
640 self
.control
, control
)
642 return NoResource("No such build '%d'" % num
)
643 return NoResource("really weird URL %s" % path
)
646 class StatusResourceChanges(HtmlResource
):
647 def __init__(self
, status
, changemaster
):
648 HtmlResource
.__init
__(self
)
650 self
.changemaster
= changemaster
651 def body(self
, request
):
653 data
+= "Change sources:\n"
654 sources
= list(self
.changemaster
)
658 data
+= "<li>%s</li>\n" % s
.describe()
661 data
+= "none (push only)\n"
663 def getChild(self
, path
, request
):
665 c
= self
.changemaster
.getChangeNumbered(num
)
667 return NoResource("No change number '%d'" % num
)
668 return StaticHTML(c
.asHTML(), "Change #%d" % num
)
670 textlog_stylesheet
= """
671 <style type="text/css">
673 font-family: "Courier New", courier, monotype;
676 font-family: "Courier New", courier, monotype;
679 font-family: "Courier New", courier, monotype;
683 font-family: "Courier New", courier, monotype;
691 implements(interfaces
.IStatusLogConsumer
)
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
])
706 self
.original
.write(formatted
)
707 except pb
.DeadReferenceError
:
708 self
.producing
.stopProducing()
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.
718 __implements__
= IHTMLLog
,
723 def __init__(self
, original
):
724 Resource
.__init
__(self
)
725 self
.original
= original
727 def getChild(self
, path
, request
):
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
738 data
+= "<body vlink=\"#800080\">\n"
739 texturl
= request
.childLink("text")
740 data
+= '<a href="%s">(view as text)</a><br />\n' % texturl
744 def content(self
, entries
):
745 spanfmt
= '<span class="%s">%s</span>'
747 for type, entry
in entries
:
749 if type != builder
.HEADER
:
752 data
+= spanfmt
% (builder
.ChunkTypes
[type],
756 def htmlFooter(self
):
758 data
+= "</body></html>\n"
761 def render_HEAD(self
, request
):
763 request
.setHeader("content-type", "text/plain")
765 request
.setHeader("content-type", "text/html")
767 # vague approximation, ignores markup
768 request
.setHeader("content-length", self
.original
.length
)
771 def render_GET(self
, req
):
775 req
.setHeader("content-type", "text/plain")
777 req
.setHeader("content-type", "text/html")
780 req
.write(self
.htmlHeader(req
))
782 self
.original
.subscribeConsumer(ChunkConsumer(req
, self
))
783 return server
.NOT_DONE_YET
790 self
.req
.write(self
.htmlFooter())
792 except pb
.DeadReferenceError
:
794 # break the cycle, the Request's .notifications list includes the
795 # Deferred (from req.notifyFinish) that's pointing at us.
798 components
.registerAdapter(TextLog
, interfaces
.IStatusLog
, IHTMLLog
)
801 class HTMLLog(Resource
):
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
822 implements(ICurrentBox
)
824 __implements__
= ICurrentBox
,
826 def formatETA(self
, eta
):
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.
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
:
851 if state
== "building":
858 text
.extend(self
.formatETA(eta
))
859 elif state
== "offline":
862 elif state
== "idle":
865 elif state
== "waiting":
869 # just in case I add a state and forget to update this
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()
881 text
.append("%d pending" % len(pbs
))
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
):
901 __implements__
= IBox
,
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
914 __implements__
= IBox
,
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
))
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
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
):
939 __implements__
= IBox
,
942 b
= self
.original
.getBuild()
943 urlbase
= "%s/builds/%d/step-%s" % (
944 urllib
.quote(b
.getBuilder().getName(), safe
=''),
946 urllib
.quote(self
.original
.getName(), safe
=''))
947 text
= self
.original
.getText()
949 log
.msg("getText() gave None", urlbase
)
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
)))
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
):
974 __implements__
= IBox
,
977 text
= self
.original
.getText()
978 color
= self
.original
.getColor()
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
992 __implements__
= IBox
,
995 assert interfaces
.IBuilderStatus(self
.original
)
996 b
= self
.original
.getLastFinishedBuild()
998 return Box(["none"], "white", class_
="LastBuild")
999 name
= b
.getBuilder().getName()
1000 number
= b
.getNumber()
1001 url
= "%s/builds/%d" % (name
, number
)
1004 # TODO: add link to the per-build page at 'url'
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
):
1019 __implements__
= IBox
,
1022 #b = Box(["spacer"], "white")
1026 components
.registerAdapter(SpacerBox
, Spacer
, IBox
)
1028 def insertGaps(g
, lastEventTime
, idleGap
=2):
1032 starts
, finishes
= e
.getTimes()
1033 if debug
: log
.msg("E0", starts
, finishes
)
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
)
1048 starts
, finishes
= e
.getTimes()
1049 if debug
: log
.msg("E2", starts
, finishes
)
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
1057 log
.msg(" finishes=%s, gap=%s, fES=%s" % \
1058 (finishes
, idleGap
, followingEventStarts
))
1059 yield Spacer(finishes
, followingEventStarts
)
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
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()
1076 self
.title
= "BuildBot: %s" % p
1079 def body(self
, request
):
1080 "This method builds the main waterfall display."
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
)
1094 for b
in showBuilders
:
1095 if b
not in allBuilders
:
1097 if b
in builderNames
:
1099 builderNames
.append(b
)
1101 builderNames
= allBuilders
1102 builders
= map(lambda name
: self
.status
.getBuilder(name
),
1106 return self
.body0(request
, builders
)
1107 (changeNames
, builderNames
, timestamps
, eventGrid
, sourceEvents
) = \
1108 self
.buildGrid(request
, builders
)
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
)
1120 topleft
= "last build"
1121 data
+= ' <tr class="LastBuild">\n'
1122 data
+= td(topleft
, align
="right", colspan
=2, class_
="Project")
1124 box
= ITopBox(b
).getBox()
1125 data
+= box
.td(align
="center")
1128 data
+= ' <tr class="Activity">\n'
1129 data
+= td('current activity', align
='right', colspan
=2)
1131 box
= ICurrentBox(b
).getBox(self
.status
)
1132 data
+= box
.td(align
="center")
1136 TZ
= time
.tzname
[time
.daylight
]
1137 data
+= td("time (%s)" % TZ
, align
="center", class_
="Time")
1138 name
= changeNames
[0]
1140 "<a href=\"%s\">%s</a>" % (urllib
.quote(name
, safe
=''), name
),
1141 align
="center", class_
="Change")
1142 for name
in builderNames
:
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")
1153 data
+= f(request
, changeNames
+ builderNames
, timestamps
, eventGrid
,
1156 data
+= "</table>\n"
1160 data
+= "<a href=\"http://buildbot.sourceforge.net/\">Buildbot</a>"
1161 data
+= "-%s " % version
1163 data
+= "working for the "
1165 data
+= "<a href=\"%s\">%s</a> project." % (projectURL
,
1168 data
+= "%s project." % projectName
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()))
1177 def body0(self
, request
, builders
):
1178 # build the waterfall display
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
1190 data
+= td("", colspan
=2)
1194 state
, builds
= b
.getState()
1195 if state
!= "offline":
1196 text
+= "%s<br />\n" % state
#b.getCurrentBig().text[0]
1198 text
+= "OFFLINE<br />\n"
1200 data
+= td(text
, align
="center", bgcolor
=color
)
1202 # the next row has the column headers: time, changes, builder names
1204 data
+= td("Time", align
="center")
1205 data
+= td("Changes", align
="center")
1208 "<a href=\"%s\">%s</a>" % (urllib
.quote(request
.childLink(name
)), name
),
1212 # all further rows involve timestamps, commit events, and build events
1214 data
+= td("04:00", align
="bottom")
1215 data
+= td("fred", align
="center")
1217 data
+= td("stuff", align
="center", bgcolor
="red")
1220 data
+= "</table>\n"
1223 def buildGrid(self
, request
, builders
):
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
1240 sourceGenerators
= []
1242 gen
= insertGaps(s
.eventGenerator(), lastEventTime
)
1243 sourceGenerators
.append(gen
)
1244 # get the first event
1247 event
= interfaces
.IStatusEvent(e
)
1249 log
.msg("gen %s gave1 %s" % (gen
, event
.getText()))
1250 except StopIteration:
1252 sourceEvents
.append(event
)
1255 spanLength
= 10 # ten-second chunks
1256 tooOld
= util
.now() - 12*60*60 # never show more than 12 hours
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
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
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())
1292 log
.msg("pushing", event
.getText(), event
)
1293 events
.append(event
)
1294 starts
, finishes
= event
.getTimes()
1295 firstTimestamp
= util
.earlier(firstTimestamp
, starts
)
1297 event
= sourceGenerators
[c
].next()
1298 #event = interfaces.IStatusEvent(event)
1300 log
.msg("gen[%s] gave2 %s" % (sourceNames
[c
],
1302 except StopIteration:
1305 log
.msg("finished span")
1308 # this is the last pre-span event for this source
1309 lastTimestamp
= util
.later(lastTimestamp
,
1310 event
.getTimes()[0])
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
)
1322 spanStart
= lastTimestamp
- spanLength
1326 if lastTimestamp
< tooOld
:
1329 if len(timestamps
) > maxPageLen
:
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
):
1345 for r
in range(0, len(timestamps
)):
1347 data
+= "[%s]<br />" % timestamps
[r
]
1349 assert(len(row
) == len(sourceNames
))
1350 for c
in range(0, len(row
)):
1352 data
+= "<b>%s</b><br />\n" % sourceNames
[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(),
1361 data
+= "<b>%s</b> [none]<br />\n" % sourceNames
[c
]
1364 def phase1(self
, request
, sourceNames
, timestamps
, eventGrid
,
1366 # phase1 rendering: table, but boxes do not overlap
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
):
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
)
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
1400 offset
= maxRows
- len(block
)
1405 box
= IBox(e
).getBox()
1406 box
.parms
["show_idle"] = 1
1407 data
+= box
.td(valign
="top", align
="center")
1412 def phase2(self
, request
, sourceNames
, timestamps
, eventGrid
,
1417 # first pass: figure out the height of the chunks, populate grid
1419 for i
in range(1+len(sourceNames
)):
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
):
1435 grid
[0].append(None)
1437 # timestamp goes at the bottom of the chunk
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
)
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"
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
)):
1474 assert(len(strip
) == gridlen
)
1475 if strip
[-1] == None:
1476 if sourceEvents
[i
-1]:
1477 filler
= IBox(sourceEvents
[i
-1]).getBox()
1479 # this can happen if you delete part of the build history
1480 filler
= Box(text
=["?"], align
="center")
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])
1489 for col
in range(len(grid
)):
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:
1500 # bubble the empty box up
1502 strip
[-i
].parms
['rowspan'] += 1
1505 # we are above a commit box. Leave it
1506 # be, and turn the current box into an
1508 strip
[-i
] = Box([], rowspan
=1,
1509 comment
="commit bubble")
1510 strip
[-i
].spacer
= True
1512 # we are above another empty box, which
1513 # somehow wasn't already converted.
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
1526 strip
[-i
].parms
['rowspan'] = 1
1527 # third pass: render the HTML table
1528 for i
in range(gridlen
):
1537 # Nones are left empty, rowspan should make it all fit
1542 class StatusResource(Resource
):
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
1560 waterfall
= WaterfallStatusResource(self
.status
, changemaster
,
1562 self
.putChild("", waterfall
)
1564 def render(self
, request
):
1565 request
.redirect(request
.prePathURL() + '/')
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":
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
)
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"))
1595 # running from source
1596 # the icon is sibpath(__file__, "../buildbot.png") . This is for
1598 up
= os
.path
.dirname
1599 buildbot_icon
= os
.path
.abspath(os
.path
.join(up(up(__file__
)),
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
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
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)
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
,
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
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
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
1721 self
.favicon
= favicon
1722 self
.robots_txt
= robots_txt
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
,
1732 def setServiceParent(self
, parent
):
1734 @type parent: L{buildbot.master.BuildMaster}
1736 base
.StatusReceiverMultiService
.setServiceParent(self
, parent
)
1740 status
= self
.parent
.getStatus()
1742 control
= interfaces
.IControl(self
.parent
)
1745 change_svc
= self
.parent
.change_svc
1746 sr
= StatusResource(status
, control
, change_svc
, self
.categories
,
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
)