diff options
Diffstat (limited to 'buildbot/buildbot/changes/bonsaipoller.py')
-rw-r--r-- | buildbot/buildbot/changes/bonsaipoller.py | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/buildbot/buildbot/changes/bonsaipoller.py b/buildbot/buildbot/changes/bonsaipoller.py new file mode 100644 index 0000000..2e319bb --- /dev/null +++ b/buildbot/buildbot/changes/bonsaipoller.py @@ -0,0 +1,320 @@ +import time +from xml.dom import minidom + +from twisted.python import log, failure +from twisted.internet import reactor +from twisted.internet.task import LoopingCall +from twisted.web.client import getPage + +from buildbot.changes import base, changes + +class InvalidResultError(Exception): + def __init__(self, value="InvalidResultError"): + self.value = value + def __str__(self): + return repr(self.value) + +class EmptyResult(Exception): + pass + +class NoMoreCiNodes(Exception): + pass + +class NoMoreFileNodes(Exception): + pass + +class BonsaiResult: + """I hold a list of CiNodes""" + def __init__(self, nodes=[]): + self.nodes = nodes + + def __cmp__(self, other): + if len(self.nodes) != len(other.nodes): + return False + for i in range(len(self.nodes)): + if self.nodes[i].log != other.nodes[i].log \ + or self.nodes[i].who != other.nodes[i].who \ + or self.nodes[i].date != other.nodes[i].date \ + or len(self.nodes[i].files) != len(other.nodes[i].files): + return -1 + + for j in range(len(self.nodes[i].files)): + if self.nodes[i].files[j].revision \ + != other.nodes[i].files[j].revision \ + or self.nodes[i].files[j].filename \ + != other.nodes[i].files[j].filename: + return -1 + + return 0 + +class CiNode: + """I hold information baout one <ci> node, including a list of files""" + def __init__(self, log="", who="", date=0, files=[]): + self.log = log + self.who = who + self.date = date + self.files = files + +class FileNode: + """I hold information about one <f> node""" + def __init__(self, revision="", filename=""): + self.revision = revision + self.filename = filename + +class BonsaiParser: + """I parse the XML result from a bonsai cvsquery.""" + + def __init__(self, data): + try: + # this is a fix for non-ascii characters + # because bonsai does not give us an encoding to work with + # it impossible to be 100% sure what to decode it as but latin1 covers + # the broadest base + data = data.decode("latin1") + data = data.encode("ascii", "replace") + self.dom = minidom.parseString(data) + log.msg(data) + except: + raise InvalidResultError("Malformed XML in result") + + self.ciNodes = self.dom.getElementsByTagName("ci") + self.currentCiNode = None # filled in by _nextCiNode() + self.fileNodes = None # filled in by _nextCiNode() + self.currentFileNode = None # filled in by _nextFileNode() + self.bonsaiResult = self._parseData() + + def getData(self): + return self.bonsaiResult + + def _parseData(self): + """Returns data from a Bonsai cvsquery in a BonsaiResult object""" + nodes = [] + try: + while self._nextCiNode(): + files = [] + try: + while self._nextFileNode(): + files.append(FileNode(self._getRevision(), + self._getFilename())) + except NoMoreFileNodes: + pass + except InvalidResultError: + raise + cinode = CiNode(self._getLog(), self._getWho(), + self._getDate(), files) + # hack around bonsai xml output bug for empty check-in comments + if not cinode.log and nodes and \ + not nodes[-1].log and \ + cinode.who == nodes[-1].who and \ + cinode.date == nodes[-1].date: + nodes[-1].files += cinode.files + else: + nodes.append(cinode) + + except NoMoreCiNodes: + pass + except InvalidResultError, EmptyResult: + raise + + return BonsaiResult(nodes) + + + def _nextCiNode(self): + """Iterates to the next <ci> node and fills self.fileNodes with + child <f> nodes""" + try: + self.currentCiNode = self.ciNodes.pop(0) + if len(self.currentCiNode.getElementsByTagName("files")) > 1: + raise InvalidResultError("Multiple <files> for one <ci>") + + self.fileNodes = self.currentCiNode.getElementsByTagName("f") + except IndexError: + # if there was zero <ci> nodes in the result + if not self.currentCiNode: + raise EmptyResult + else: + raise NoMoreCiNodes + + return True + + def _nextFileNode(self): + """Iterates to the next <f> node""" + try: + self.currentFileNode = self.fileNodes.pop(0) + except IndexError: + raise NoMoreFileNodes + + return True + + def _getLog(self): + """Returns the log of the current <ci> node""" + logs = self.currentCiNode.getElementsByTagName("log") + if len(logs) < 1: + raise InvalidResultError("No log present") + elif len(logs) > 1: + raise InvalidResultError("Multiple logs present") + + # catch empty check-in comments + if logs[0].firstChild: + return logs[0].firstChild.data + return '' + + def _getWho(self): + """Returns the e-mail address of the commiter""" + # convert unicode string to regular string + return str(self.currentCiNode.getAttribute("who")) + + def _getDate(self): + """Returns the date (unix time) of the commit""" + # convert unicode number to regular one + try: + commitDate = int(self.currentCiNode.getAttribute("date")) + except ValueError: + raise InvalidResultError + + return commitDate + + def _getFilename(self): + """Returns the filename of the current <f> node""" + try: + filename = self.currentFileNode.firstChild.data + except AttributeError: + raise InvalidResultError("Missing filename") + + return filename + + def _getRevision(self): + return self.currentFileNode.getAttribute("rev") + + +class BonsaiPoller(base.ChangeSource): + """This source will poll a bonsai server for changes and submit + them to the change master.""" + + compare_attrs = ["bonsaiURL", "pollInterval", "tree", + "module", "branch", "cvsroot"] + + parent = None # filled in when we're added + loop = None + volatile = ['loop'] + working = False + + def __init__(self, bonsaiURL, module, branch, tree="default", + cvsroot="/cvsroot", pollInterval=30): + """ + @type bonsaiURL: string + @param bonsaiURL: The base URL of the Bonsai server + (ie. http://bonsai.mozilla.org) + @type module: string + @param module: The module to look for changes in. Commonly + this is 'all' + @type branch: string + @param branch: The branch to look for changes in. This must + match the + 'branch' option for the Scheduler. + @type tree: string + @param tree: The tree to look for changes in. Commonly this + is 'all' + @type cvsroot: string + @param cvsroot: The cvsroot of the repository. Usually this is + '/cvsroot' + @type pollInterval: int + @param pollInterval: The time (in seconds) between queries for + changes + """ + + self.bonsaiURL = bonsaiURL + self.module = module + self.branch = branch + self.tree = tree + self.cvsroot = cvsroot + self.pollInterval = pollInterval + self.lastChange = time.time() + self.lastPoll = time.time() + + def startService(self): + self.loop = LoopingCall(self.poll) + base.ChangeSource.startService(self) + + reactor.callLater(0, self.loop.start, self.pollInterval) + + def stopService(self): + self.loop.stop() + return base.ChangeSource.stopService(self) + + def describe(self): + str = "" + str += "Getting changes from the Bonsai service running at %s " \ + % self.bonsaiURL + str += "<br>Using tree: %s, branch: %s, and module: %s" % (self.tree, \ + self.branch, self.module) + return str + + def poll(self): + if self.working: + log.msg("Not polling Bonsai because last poll is still working") + else: + self.working = True + d = self._get_changes() + d.addCallback(self._process_changes) + d.addCallbacks(self._finished_ok, self._finished_failure) + return + + def _finished_ok(self, res): + assert self.working + self.working = False + + # check for failure -- this is probably never hit but the twisted docs + # are not clear enough to be sure. it is being kept "just in case" + if isinstance(res, failure.Failure): + log.msg("Bonsai poll failed: %s" % res) + return res + + def _finished_failure(self, res): + log.msg("Bonsai poll failed: %s" % res) + assert self.working + self.working = False + return None # eat the failure + + def _make_url(self): + args = ["treeid=%s" % self.tree, "module=%s" % self.module, + "branch=%s" % self.branch, "branchtype=match", + "sortby=Date", "date=explicit", + "mindate=%d" % self.lastChange, + "maxdate=%d" % int(time.time()), + "cvsroot=%s" % self.cvsroot, "xml=1"] + # build the bonsai URL + url = self.bonsaiURL + url += "/cvsquery.cgi?" + url += "&".join(args) + + return url + + def _get_changes(self): + url = self._make_url() + log.msg("Polling Bonsai tree at %s" % url) + + self.lastPoll = time.time() + # get the page, in XML format + return getPage(url, timeout=self.pollInterval) + + def _process_changes(self, query): + try: + bp = BonsaiParser(query) + result = bp.getData() + except InvalidResultError, e: + log.msg("Could not process Bonsai query: " + e.value) + return + except EmptyResult: + return + + for cinode in result.nodes: + files = [file.filename + ' (revision '+file.revision+')' + for file in cinode.files] + c = changes.Change(who = cinode.who, + files = files, + comments = cinode.log, + when = cinode.date, + branch = self.branch) + self.parent.addChange(c) + self.lastChange = self.lastPoll |