OLD | NEW |
| (Empty) |
1 import time | |
2 from xml.dom import minidom | |
3 | |
4 from twisted.python import log, failure | |
5 from twisted.internet import reactor | |
6 from twisted.internet.task import LoopingCall | |
7 from twisted.web.client import getPage | |
8 | |
9 from buildbot.changes import base, changes | |
10 | |
11 class InvalidResultError(Exception): | |
12 def __init__(self, value="InvalidResultError"): | |
13 self.value = value | |
14 def __str__(self): | |
15 return repr(self.value) | |
16 | |
17 class EmptyResult(Exception): | |
18 pass | |
19 | |
20 class NoMoreCiNodes(Exception): | |
21 pass | |
22 | |
23 class NoMoreFileNodes(Exception): | |
24 pass | |
25 | |
26 class BonsaiResult: | |
27 """I hold a list of CiNodes""" | |
28 def __init__(self, nodes=[]): | |
29 self.nodes = nodes | |
30 | |
31 def __cmp__(self, other): | |
32 if len(self.nodes) != len(other.nodes): | |
33 return False | |
34 for i in range(len(self.nodes)): | |
35 if self.nodes[i].log != other.nodes[i].log \ | |
36 or self.nodes[i].who != other.nodes[i].who \ | |
37 or self.nodes[i].date != other.nodes[i].date \ | |
38 or len(self.nodes[i].files) != len(other.nodes[i].files): | |
39 return -1 | |
40 | |
41 for j in range(len(self.nodes[i].files)): | |
42 if self.nodes[i].files[j].revision \ | |
43 != other.nodes[i].files[j].revision \ | |
44 or self.nodes[i].files[j].filename \ | |
45 != other.nodes[i].files[j].filename: | |
46 return -1 | |
47 | |
48 return 0 | |
49 | |
50 class CiNode: | |
51 """I hold information baout one <ci> node, including a list of files""" | |
52 def __init__(self, log="", who="", date=0, files=[]): | |
53 self.log = log | |
54 self.who = who | |
55 self.date = date | |
56 self.files = files | |
57 | |
58 class FileNode: | |
59 """I hold information about one <f> node""" | |
60 def __init__(self, revision="", filename=""): | |
61 self.revision = revision | |
62 self.filename = filename | |
63 | |
64 class BonsaiParser: | |
65 """I parse the XML result from a bonsai cvsquery.""" | |
66 | |
67 def __init__(self, data): | |
68 try: | |
69 # this is a fix for non-ascii characters | |
70 # because bonsai does not give us an encoding to work with | |
71 # it impossible to be 100% sure what to decode it as but latin1 covers | |
72 # the broadest base | |
73 data = data.decode("latin1") | |
74 data = data.encode("ascii", "replace") | |
75 self.dom = minidom.parseString(data) | |
76 log.msg(data) | |
77 except: | |
78 raise InvalidResultError("Malformed XML in result") | |
79 | |
80 self.ciNodes = self.dom.getElementsByTagName("ci") | |
81 self.currentCiNode = None # filled in by _nextCiNode() | |
82 self.fileNodes = None # filled in by _nextCiNode() | |
83 self.currentFileNode = None # filled in by _nextFileNode() | |
84 self.bonsaiResult = self._parseData() | |
85 | |
86 def getData(self): | |
87 return self.bonsaiResult | |
88 | |
89 def _parseData(self): | |
90 """Returns data from a Bonsai cvsquery in a BonsaiResult object""" | |
91 nodes = [] | |
92 try: | |
93 while self._nextCiNode(): | |
94 files = [] | |
95 try: | |
96 while self._nextFileNode(): | |
97 files.append(FileNode(self._getRevision(), | |
98 self._getFilename())) | |
99 except NoMoreFileNodes: | |
100 pass | |
101 except InvalidResultError: | |
102 raise | |
103 cinode = CiNode(self._getLog(), self._getWho(), | |
104 self._getDate(), files) | |
105 # hack around bonsai xml output bug for empty check-in comments | |
106 if not cinode.log and nodes and \ | |
107 not nodes[-1].log and \ | |
108 cinode.who == nodes[-1].who and \ | |
109 cinode.date == nodes[-1].date: | |
110 nodes[-1].files += cinode.files | |
111 else: | |
112 nodes.append(cinode) | |
113 | |
114 except NoMoreCiNodes: | |
115 pass | |
116 except InvalidResultError, EmptyResult: | |
117 raise | |
118 | |
119 return BonsaiResult(nodes) | |
120 | |
121 | |
122 def _nextCiNode(self): | |
123 """Iterates to the next <ci> node and fills self.fileNodes with | |
124 child <f> nodes""" | |
125 try: | |
126 self.currentCiNode = self.ciNodes.pop(0) | |
127 if len(self.currentCiNode.getElementsByTagName("files")) > 1: | |
128 raise InvalidResultError("Multiple <files> for one <ci>") | |
129 | |
130 self.fileNodes = self.currentCiNode.getElementsByTagName("f") | |
131 except IndexError: | |
132 # if there was zero <ci> nodes in the result | |
133 if not self.currentCiNode: | |
134 raise EmptyResult | |
135 else: | |
136 raise NoMoreCiNodes | |
137 | |
138 return True | |
139 | |
140 def _nextFileNode(self): | |
141 """Iterates to the next <f> node""" | |
142 try: | |
143 self.currentFileNode = self.fileNodes.pop(0) | |
144 except IndexError: | |
145 raise NoMoreFileNodes | |
146 | |
147 return True | |
148 | |
149 def _getLog(self): | |
150 """Returns the log of the current <ci> node""" | |
151 logs = self.currentCiNode.getElementsByTagName("log") | |
152 if len(logs) < 1: | |
153 raise InvalidResultError("No log present") | |
154 elif len(logs) > 1: | |
155 raise InvalidResultError("Multiple logs present") | |
156 | |
157 # catch empty check-in comments | |
158 if logs[0].firstChild: | |
159 return logs[0].firstChild.data | |
160 return '' | |
161 | |
162 def _getWho(self): | |
163 """Returns the e-mail address of the commiter""" | |
164 # convert unicode string to regular string | |
165 return str(self.currentCiNode.getAttribute("who")) | |
166 | |
167 def _getDate(self): | |
168 """Returns the date (unix time) of the commit""" | |
169 # convert unicode number to regular one | |
170 try: | |
171 commitDate = int(self.currentCiNode.getAttribute("date")) | |
172 except ValueError: | |
173 raise InvalidResultError | |
174 | |
175 return commitDate | |
176 | |
177 def _getFilename(self): | |
178 """Returns the filename of the current <f> node""" | |
179 try: | |
180 filename = self.currentFileNode.firstChild.data | |
181 except AttributeError: | |
182 raise InvalidResultError("Missing filename") | |
183 | |
184 return filename | |
185 | |
186 def _getRevision(self): | |
187 return self.currentFileNode.getAttribute("rev") | |
188 | |
189 | |
190 class BonsaiPoller(base.ChangeSource): | |
191 """This source will poll a bonsai server for changes and submit | |
192 them to the change master.""" | |
193 | |
194 compare_attrs = ["bonsaiURL", "pollInterval", "tree", | |
195 "module", "branch", "cvsroot"] | |
196 | |
197 parent = None # filled in when we're added | |
198 loop = None | |
199 volatile = ['loop'] | |
200 working = False | |
201 | |
202 def __init__(self, bonsaiURL, module, branch, tree="default", | |
203 cvsroot="/cvsroot", pollInterval=30): | |
204 """ | |
205 @type bonsaiURL: string | |
206 @param bonsaiURL: The base URL of the Bonsai server | |
207 (ie. http://bonsai.mozilla.org) | |
208 @type module: string | |
209 @param module: The module to look for changes in. Commonly | |
210 this is 'all' | |
211 @type branch: string | |
212 @param branch: The branch to look for changes in. This must | |
213 match the | |
214 'branch' option for the Scheduler. | |
215 @type tree: string | |
216 @param tree: The tree to look for changes in. Commonly this | |
217 is 'all' | |
218 @type cvsroot: string | |
219 @param cvsroot: The cvsroot of the repository. Usually this is | |
220 '/cvsroot' | |
221 @type pollInterval: int | |
222 @param pollInterval: The time (in seconds) between queries for | |
223 changes | |
224 """ | |
225 | |
226 self.bonsaiURL = bonsaiURL | |
227 self.module = module | |
228 self.branch = branch | |
229 self.tree = tree | |
230 self.cvsroot = cvsroot | |
231 self.pollInterval = pollInterval | |
232 self.lastChange = time.time() | |
233 self.lastPoll = time.time() | |
234 | |
235 def startService(self): | |
236 self.loop = LoopingCall(self.poll) | |
237 base.ChangeSource.startService(self) | |
238 | |
239 reactor.callLater(0, self.loop.start, self.pollInterval) | |
240 | |
241 def stopService(self): | |
242 self.loop.stop() | |
243 return base.ChangeSource.stopService(self) | |
244 | |
245 def describe(self): | |
246 str = "" | |
247 str += "Getting changes from the Bonsai service running at %s " \ | |
248 % self.bonsaiURL | |
249 str += "<br>Using tree: %s, branch: %s, and module: %s" % (self.tree, \ | |
250 self.branch, self.module) | |
251 return str | |
252 | |
253 def poll(self): | |
254 if self.working: | |
255 log.msg("Not polling Bonsai because last poll is still working") | |
256 else: | |
257 self.working = True | |
258 d = self._get_changes() | |
259 d.addCallback(self._process_changes) | |
260 d.addCallbacks(self._finished_ok, self._finished_failure) | |
261 return | |
262 | |
263 def _finished_ok(self, res): | |
264 assert self.working | |
265 self.working = False | |
266 | |
267 # check for failure -- this is probably never hit but the twisted docs | |
268 # are not clear enough to be sure. it is being kept "just in case" | |
269 if isinstance(res, failure.Failure): | |
270 log.msg("Bonsai poll failed: %s" % res) | |
271 return res | |
272 | |
273 def _finished_failure(self, res): | |
274 log.msg("Bonsai poll failed: %s" % res) | |
275 assert self.working | |
276 self.working = False | |
277 return None # eat the failure | |
278 | |
279 def _make_url(self): | |
280 args = ["treeid=%s" % self.tree, "module=%s" % self.module, | |
281 "branch=%s" % self.branch, "branchtype=match", | |
282 "sortby=Date", "date=explicit", | |
283 "mindate=%d" % self.lastChange, | |
284 "maxdate=%d" % int(time.time()), | |
285 "cvsroot=%s" % self.cvsroot, "xml=1"] | |
286 # build the bonsai URL | |
287 url = self.bonsaiURL | |
288 url += "/cvsquery.cgi?" | |
289 url += "&".join(args) | |
290 | |
291 return url | |
292 | |
293 def _get_changes(self): | |
294 url = self._make_url() | |
295 log.msg("Polling Bonsai tree at %s" % url) | |
296 | |
297 self.lastPoll = time.time() | |
298 # get the page, in XML format | |
299 return getPage(url, timeout=self.pollInterval) | |
300 | |
301 def _process_changes(self, query): | |
302 try: | |
303 bp = BonsaiParser(query) | |
304 result = bp.getData() | |
305 except InvalidResultError, e: | |
306 log.msg("Could not process Bonsai query: " + e.value) | |
307 return | |
308 except EmptyResult: | |
309 return | |
310 | |
311 for cinode in result.nodes: | |
312 files = [file.filename + ' (revision '+file.revision+')' | |
313 for file in cinode.files] | |
314 c = changes.Change(who = cinode.who, | |
315 files = files, | |
316 comments = cinode.log, | |
317 when = cinode.date, | |
318 branch = self.branch) | |
319 self.parent.addChange(c) | |
320 self.lastChange = self.lastPoll | |
OLD | NEW |