Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(325)

Side by Side Diff: tryconsole/tryconsole.py

Issue 517046: Initial check-in of tryconsole. (Closed) Base URL: svn://chrome-svn/chrome/trunk/tools/
Patch Set: '' Created 10 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tryconsole/templates/status.html ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/python
2 # Copyright (c) 2009-2010 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """AJAX-y console for try jobs."""
7
8 import datetime
9 import logging
10 import os
11 import pickle
12 import re
13 import time
14 from google.appengine.api import memcache
15 from google.appengine.api import urlfetch
16 from google.appengine.api.labs import taskqueue
17 from google.appengine.ext import db
18 from google.appengine.ext import webapp
19 from google.appengine.ext.webapp import template
20 from google.appengine.ext.webapp.util import run_wsgi_app
21
22
23 BUILDBOT_URLS = [
24 'http://build.chromium.org/buildbot/try-server',
25 ]
26 HISTORY_SIZE = 50
27
28
29 class BuildResult(db.Model):
30 """Entity for storing the current status of a single trybot build."""
31 # TODO(bradnelson): make these required?
32 master_url = db.StringProperty()
33 builder_name = db.StringProperty()
34 build_number = db.IntegerProperty()
35
36 slave_name = db.StringProperty()
37
38 raw_reason = db.StringProperty()
39 username = db.StringProperty()
40 patch_name = db.StringProperty()
41
42 revision = db.IntegerProperty()
43 got_revision = db.IntegerProperty()
44
45 account_name = db.StringProperty()
46
47 changed_at = db.DateTimeProperty()
48 start = db.DateTimeProperty()
49 end = db.DateTimeProperty()
50
51 steps_and_logfiles = db.BlobProperty()
52
53 eta_seconds = db.IntegerProperty()
54 page_built = db.DateTimeProperty()
55 last_checked = db.DateTimeProperty()
56
57
58 class PerformanceSamples(db.Model):
59 """A Sample of builds running, used to generate graphs."""
60 interval = db.IntegerProperty()
61 running_builds = db.BlobProperty()
62 measured_at = db.DateTimeProperty()
63
64
65 def GetBuilders(master_url):
66 """Given the URL of a trybot master, get a list of builders for that master.
67
68 Arguments:
69 master_url: the URL of the master.
70 Returns:
71 A list of strings containing the builder names.
72 """
73 # TODO(bradnelson): use the json interface in place of html scraping.
74 # Returned cached copy if we have one.
75 blist = memcache.get(master_url + '/builders')
76 if blist:
77 return blist
78 # Grab the webpage.
79 url = master_url + '/builders'
80 try:
81 result = urlfetch.fetch(url)
82 except urlfetch.DownloadError:
83 return []
84 if result.status_code != 200:
85 return []
86 # Pick out the proper list.
87 text = re.search('<ol>(.*)</ol>', result.content, re.DOTALL)
88 if not text:
89 return []
90 # Pick out the builder name.
91 blist = re.findall('<li><a href[^>]+>([^<]+)</a></li>', text.group(1))
92 # Add to cache.
93 memcache.add('/builders', blist, 1000)
94 return blist
95
96
97 def GetBuilderInfo(master_url, builder_name):
98 """Given a trybot master and builder name, get information about a builder.
99
100 Arguments:
101 master_url: the URL of the master.
102 builder_name: the name of a builder on this master.
103 Returns:
104 A dict filled with a range of values that describe the state of this
105 builder. It contains things like which builds are currently running.
106 """
107 # TODO(bradnelson): use the json interface in place of html scraping.
108 # Returned cached copy if we have one.
109 key = '%s/builders/%s' % (master_url, builder_name)
110 info = memcache.get(key)
111 if info:
112 return info
113 # Prep it.
114 info = {
115 'master_url': master_url,
116 'name': builder_name,
117 'currently_building': [],
118 'recent_builds': [],
119 }
120 # Grab the webpage.
121 url = master_url + '/builders/' + builder_name
122 try:
123 result = urlfetch.fetch(url)
124 except urlfetch.DownloadError:
125 return info
126 if result.status_code != 200:
127 return info
128 # Pick out the currently building list.
129 text = re.search('<h2>Currently Building:</h2>[ \n\r]+<ul>(.*?)</ul>',
130 result.content, re.DOTALL)
131 if text:
132 # Pick out info about each one currently building.
133 builders = re.findall(r'<li>([^:]+): <a[^>]+>#([0-9]+)</a> '
134 r'ETA ([0-9]+)s \(([^)]+)\) \[([^]]+)\]</li>',
135 text.group(1))
136 for b in builders:
137 e = {
138 'slave_name': str(b[0]),
139 'build_number': int(b[1]),
140 'eta_seconds': int(b[2]),
141 'eta_time': str(b[3]),
142 'stage': str(b[4]),
143 }
144 info['currently_building'].append(e)
145 # Pick out the recently built list.
146 text = re.search('<h2>Recent Builds:</h2>[ \n\r]+<ul>(.*?)</ul>',
147 result.content, re.DOTALL)
148 if text:
149 # Pick out info about each.
150 builders = re.findall(r'>#([0-9]+)</a>:', text.group(1))
151 for b in builders:
152 e = {
153 'build_number': int(b),
154 }
155 info['recent_builds'].append(e)
156 # Find highest builder number.
157 build_nums = [0]
158 build_nums += [i['build_number'] for i in info['currently_building']]
159 build_nums += [i['build_number'] for i in info['recent_builds']]
160 info['latest'] = max(build_nums)
161 # Add to cache.
162 memcache.add(key, info, 1)
163 return info
164
165
166 def CheckForBuild(master_url, builder_name, build_number):
167 """Check if a particular build exists.
168
169 Arguments:
170 master_url: the URL of the master.
171 builder_name: the name of a builder on this master.
172 build_number: integer builder number.
173 Returns:
174 True or False depending on if this build exists in the data-store.
175 """
176 q = db.GqlQuery('SELECT __key__ FROM BuildResult '
177 'WHERE master_url=:1 '
178 'AND builder_name=:2 '
179 'AND build_number=:3',
180 master_url, builder_name, build_number)
181 b = q.fetch(1)
182 if not b or len(b) != 1:
183 return False
184 else:
185 return True
186
187
188 def UpdateBuildResult(br):
189 """Update data-store information on a particular build.
190
191 Checks the master for fresh information on a given build.
192 Arguments:
193 br: A build result to update.
194 """
195 # TODO(bradnelson): use the json interface in place of html scraping.
196 # Advance time.
197 br.last_checked = datetime.datetime.now()
198 # Grab the webpage.
199 url = '%s/builders/%s/builds/%d' % (
200 br.master_url, br.builder_name, br.build_number)
201 try:
202 result = urlfetch.fetch(url)
203 except urlfetch.DownloadError:
204 br.put()
205 logging.debug('Download Error Updating %s | %s | %d',
206 br.master_url, br.builder_name, br.build_number)
207 return
208 # Handle 404 specially.
209 if result.status_code == 404:
210 # Put in an empty entry for expired ones.
211 # But only do it if we have nothing else.
212 if not CheckForBuild(br.master_url, br.builder_name, br.build_number):
213 br.end = datetime.datetime.fromordinal(1)
214 br.put()
215 if result.status_code != 200:
216 br.put()
217 logging.debug('Result Code %d Updating %s | %s | %d',
218 result.status_code,
219 br.master_url, br.builder_name, br.build_number)
220 return
221
222 # Pick out content.
223 content = result.content
224
225 # Pick out steps and logfiles.
226 text = re.search('<h2>Build In Progress</h2><div>'
227 r'ETA ([0-9]+)s \(([^)]+)\)</div>', content)
228 if text:
229 br.eta_seconds = int(text.group(1))
230 else:
231 br.eta_seconds = 0
232 # Pick out steps and logfiles.
233 text = re.search('<h2>Steps and Logfiles:</h2>[ \n\r]+'
234 '<ol>(.+?)</ol>[ \n\r]*<h2>',
235 content, re.DOTALL)
236 if text:
237 br.steps_and_logfiles = text.group(1)
238 # Pick out reason.
239 text = re.search('<h2>Reason:</h2>[\n\r]+([^\n\r].+?)[\n\r]+<h2>',
240 content, re.DOTALL)
241 if text:
242 br.raw_reason = text.group(1)
243 m = re.match("'([^:]+): ([^']+)' try job", br.raw_reason)
244 if m:
245 br.username = m.group(1)
246 br.patch_name = m.group(2)
247 # Pick out source stamp.
248 text = re.search('<h2>SourceStamp:</h2>[\n\r ]+<ul>(.+?)</ul>',
249 content, re.DOTALL)
250 if text:
251 m = re.search('<li>Revision: ([0-9]+)</li>', text.group(1))
252 if m:
253 br.revision = int(m.group(1))
254 m = re.search('<li>Got Revision: ([0-9]+)</li>', text.group(1))
255 if m:
256 br.got_revision = int(m.group(1))
257 # Pick out all changes.
258 text = re.search('<h2>All Changes</h2>[\n\r ]+<ol>(.+?)</ol>',
259 content, re.DOTALL)
260 if text:
261 # Pick out who changed it.
262 m = re.search('<p>Changed by: <b>(.+?)</b><br />', text.group(1))
263 if m:
264 br.account_name = m.group(1)
265 # Pick out when the change occured.
266 m = re.search('Changed at: <b>(.+?)</b><br />', text.group(1))
267 if m:
268 tm = time.strptime(m.group(1), '%a %d %b %Y %H:%M:%S')
269 br.changed_at = datetime.datetime(*(tm)[0:6])
270 # Pick out timing.
271 text = re.search('<h2>Timing</h2>[\n\r ]+<table>(.+?)</table>',
272 content, re.DOTALL)
273 if text:
274 m = re.search('<tr><td>Start</td><td>(.+?)</td></tr>', text.group(1))
275 if m:
276 tm = time.strptime(m.group(1), '%a %b %d %H:%M:%S %Y')
277 br.start = datetime.datetime(*(tm)[0:6])
278 m = re.search('<tr><td>End</td><td>(.+?)</td></tr>', text.group(1))
279 if m:
280 tm = time.strptime(m.group(1), '%a %b %d %H:%M:%S %Y')
281 br.end = datetime.datetime(*(tm)[0:6])
282 # Pick page built.
283 text = re.search('Page built: (.+?)[\n\r ]+</div>',
284 content, re.DOTALL)
285 if text:
286 tm = time.strptime(text.group(1), '%a %d %b %Y %H:%M:%S')
287 br.page_built = datetime.datetime(*(tm)[0:6])
288 # Store it.
289 br.put()
290 # Log it.
291 logging.debug('Updating %s | %s | %d',
292 br.master_url, br.builder_name, br.build_number)
293
294
295 def IntroduceBuildResult(master_url, builder_name, build_number):
296 """Introduces information into the data-store on a particular build.
297
298 Checks the master for fresh information on a given build.
299 Arguments:
300 master_url: the URL of the master.
301 builder_name: the name of a builder on this master.
302 build_number: integer builder number.
303 """
304 # Get the current entry if any.
305 q = BuildResult.gql('WHERE master_url=:1 and '
306 'builder_name=:2 and '
307 'build_number=:3',
308 master_url, builder_name, build_number)
309 br = q.fetch(1)
310 if br:
311 br = br[0]
312 else:
313 br = BuildResult()
314 # Set what we know.
315 br.master_url = master_url
316 br.builder_name = builder_name
317 br.build_number = build_number
318 # Update it.
319 UpdateBuildResult(br)
320
321
322 class UpdateBuilders(webapp.RequestHandler):
323 """Handles web requests to update all builders on a given trybot master."""
324
325 def post(self):
326 # Update info on the current set of builders.
327 master_url = self.request.get('master_url')
328 builders = GetBuilders(master_url)
329 for builder_name in builders:
330 taskqueue.Task(
331 url='/update_builder',
332 params={'master_url': master_url,
333 'builder_name': builder_name}).add(
334 queue_name='update-builder')
335
336
337 def GetLatestBuilderBuild(master_url, builder_name):
338 """Given a builder, find the latest build number for it.
339
340 Arguments:
341 master_url: the URL of the master.
342 builder_name: the name of a builder on this master.
343 Returns:
344 The integer value of the latest build number for this builder.
345 """
346 q = BuildResult.gql('WHERE master_url=:1 '
347 'AND builder_name=:2 '
348 'ORDER BY build_number DESC',
349 master_url, builder_name)
350 latest = q.fetch(1)
351 if not latest or len(latest) != 1:
352 return 0
353 else:
354 return latest[0].build_number
355
356
357 class UpdateBuilder(webapp.RequestHandler):
358 """Handle a web request to poll the status of a given builder."""
359
360 def post(self):
361 """Handle post requests."""
362 # Update info on one builder.
363 master_url = self.request.get('master_url')
364 builder_name = self.request.get('builder_name')
365 # Get latest build known to builder.
366 info = GetBuilderInfo(master_url, builder_name)
367 latest_known = info.get('latest', 1)
368 # Get latest build in the datastore.
369 latest_stored = GetLatestBuilderBuild(master_url, builder_name)
370 # We're done if they match,.
371 if latest_stored >= latest_known: return
372 # Add a task to introduce up to that build.
373 taskqueue.Task(
374 url='/introduce_build',
375 params={'master_url': master_url,
376 'builder_name': builder_name,
377 'begin': str(latest_stored + 1),
378 'end': str(latest_known)}).add(
379 queue_name='introduce-build')
380
381
382 class IntroduceBuild(webapp.RequestHandler):
383 """Handles a web request to introduce a new build to the datastore."""
384
385 def get(self):
386 """Handle get requests as post requests."""
387 self.post()
388
389 def post(self):
390 """Handle post requests."""
391 # Update info on one builder.
392 master_url = self.request.get('master_url')
393 builder_name = self.request.get('builder_name')
394 begin = int(self.request.get('begin'))
395 end = int(self.request.get('end'))
396 # Done if begin > end.
397 if begin > end: return
398 # Update one past current one.
399 IntroduceBuildResult(master_url, builder_name, begin)
400 # Log it.
401 logging.debug('Introducing %s | %s | %d',
402 master_url, builder_name, begin)
403 # Advance to the next one.
404 begin += 1
405 # Done if begin > end.
406 if begin > end: return
407 # Trigger self again.
408 taskqueue.Task(
409 url='/introduce_build',
410 params={'master_url': master_url,
411 'builder_name': builder_name,
412 'begin': str(begin),
413 'end': str(end)}).add(
414 queue_name='introduce-build')
415
416
417 class UpdateBuilds(webapp.RequestHandler):
418 """Handle a web request to update the most stale build still in progress."""
419
420 def post(self):
421 """Handle post requests."""
422 # Update info on one build.
423 q = BuildResult.gql('WHERE end=null ORDER BY last_checked')
424 stale = q.fetch(1)
425 if not stale:
426 return
427 for br in stale:
428 UpdateBuildResult(br)
429
430
431 def GetBuildListFromDB(count):
432 """Fetch a list of the keys for the most recent builds.
433
434 Arguments:
435 count: a limit on the number of builds to fetch.
436 Returns:
437 A list of database keys for the latest builds.
438 """
439 q = db.GqlQuery('SELECT __key__ FROM BuildResult ORDER BY start DESC')
440 builds = q.fetch(count)
441 # List out the keys for these builds.
442 info = ''
443 for b in builds:
444 info += '%s\n' % b
445 return info
446
447
448 def GetBuildList(count):
449 """Fetch a list of the keys for the most recent builds, cached.
450
451 Cache recent builds in the memcache, to reduce load on the data-store.
452 Arguments:
453 count: a limit on the number of builds to fetch.
454 Returns:
455 A list of database keys for the latest builds.
456 """
457 key = 'listbuilds//%d' % count
458 info = memcache.get(key)
459 if not info:
460 info = GetBuildListFromDB(count)
461 memcache.set(key, info, 20)
462 return info
463
464
465 class ListBuilds(webapp.RequestHandler):
466 """Handle a web request for the latest builds."""
467
468 def get(self):
469 """Handle get requests."""
470 self.response.headers['Content-Type'] = 'text/plain'
471 # Get a different number of builds based on if this is the initial set.
472 if self.request.get('initial', 0):
473 info = GetBuildList(300)
474 else:
475 info = GetBuildList(50)
476 # Write it out.
477 self.response.out.write(info)
478
479
480 class BuilderDump(webapp.RequestHandler):
481 """Handle a web request for information on a particular builder."""
482
483 def get(self):
484 """Handle get requests."""
485 self.response.headers['Content-Type'] = 'text/plain'
486 q = BuildResult.gql('WHERE builder_name=:1 '
487 'and build_number >= :2 '
488 'ORDER BY build_number',
489 self.request.get('builder'),
490 int(self.request.get('start')))
491 builds = q.fetch(30)
492 for i in builds:
493 self.response.out.write('%d\n' % i.build_number)
494
495
496 class BuildInfo(webapp.RequestHandler):
497 """Handle a web request for information on a particular build."""
498
499 def get(self):
500 """Handle get requests."""
501 self.response.headers['Content-Type'] = 'text/plain'
502 build_id = str(self.request.get('id'))
503 # Decide the key to use for the memory cache.
504 key = 'buildinfo3//' + build_id
505 # Try to get a cached copy.
506 cinfo = memcache.get(key)
507 if cinfo:
508 cache_on_client = cinfo[0]
509 info = cinfo[1]
510 else:
511 # Load from datastore.
512 br = BuildResult.get(build_id)
513 if not br: return
514 # Write out info on this build.
515 info = ''
516 info += ('master_url %s\n' % br.master_url)
517 info += ('builder_name %s\n' % br.builder_name)
518 info += ('build_number %s\n' % br.build_number)
519 info += ('slave_name %s\n' % br.slave_name)
520 info += ('raw_reason %s\n' % br.raw_reason)
521 info += ('username %s\n' % br.username)
522 info += ('patch_name %s\n' % br.patch_name)
523 info += ('got_revision %s\n' % br.got_revision)
524 info += ('account_name %s\n' % br.account_name)
525 info += ('changed_at %s\n' % br.changed_at)
526 info += ('start %s\n' % br.start)
527 info += ('end %s\n' % br.end)
528 info += ('eta_seconds %s\n' % br.eta_seconds)
529 info += ('page_built %s\n' % br.page_built)
530 info += ('last_checked %s\n' % br.last_checked)
531 logs = re.sub('[\r\n\t]', ' ', br.steps_and_logfiles)
532 info += ('steps_and_logfiles %s\n' % logs)
533 # Only allow caching on client if its done building.
534 cache_on_client = br.end is not None
535 if cache_on_client:
536 timeout = 0
537 else:
538 timeout = 15
539 memcache.set(key, (cache_on_client, info), time=timeout)
540 # Allow the client to cache it for a day, assumming its done.
541 if cache_on_client:
542 self.response.headers['Cache-Control'] = 'public, max-age=86400'
543 # Write out the info.
544 self.response.out.write(info)
545
546
547 class StalePage(webapp.RequestHandler):
548 """Return a long cached value for requests to historical stale pages."""
549
550 def get(self):
551 """Handle get requests."""
552 self.response.headers['Content-Type'] = 'text/plain'
553 self.response.headers['Cache-Control'] = 'public, max-age=86400'
554
555
556 def ChromeFrameMe(handler):
557 """Handle web requests to instruct users to install ChromeFrame.
558
559 Arguments:
560 handler: the current webapp.RequestHandler.
561 Returns:
562 True or False indicating success.
563 """
564 agent = handler.request.headers.get('USER_AGENT', '')
565 if agent.find('MSIE') >= 0 and agent.find('chromeframe') < 0:
566 path = os.path.join(os.path.dirname(__file__),
567 'templates/chrome_frame.html')
568 handler.response.out.write(template.render(path, {}))
569 return True
570 return False
571
572
573 class MainPage(webapp.RequestHandler):
574 """Handle web requests for the main tryconsole page."""
575
576 def get(self):
577 """Handle get requests."""
578 if ChromeFrameMe(self): return
579 path = os.path.join(os.path.dirname(__file__), 'templates/status.html')
580 self.response.out.write(template.render(path, {}))
581
582
583 class PerformanceRaw(webapp.RequestHandler):
584 """Handle web requests for graphed performance data."""
585
586 def get(self):
587 """Handle get requests."""
588 now = datetime.datetime.now()
589 self.response.headers['Content-Type'] = 'text/plain'
590 interval = int(self.request.get('interval'))
591 start = int(self.request.get('start'))
592 stop = int(self.request.get('stop'))
593 q = PerformanceSamples.gql('WHERE interval=:1 and '
594 'measured_at>=:2 and '
595 'measured_at<=:3 '
596 'ORDER BY measured_at',
597 interval,
598 now - datetime.timedelta(seconds=start),
599 now - datetime.timedelta(seconds=stop))
600 samples = q.fetch(1000)
601 for s in samples:
602 measured_at = int(time.mktime(s.measured_at.timetuple()))
603 srb = pickle.loads(s.running_builds)
604 for b in srb:
605 self.response.out.write(
606 '%s|%s|%d|%d|%d\n' % (b[0], b[1], measured_at,
607 srb[b]['count'], srb[b]['samples']))
608
609
610 class TallyMerge(webapp.RequestHandler):
611 """Handle web requests to generate overview performance data."""
612
613 def get(self):
614 """Handle get requests."""
615 now = datetime.datetime.now()
616 src = int(self.request.get('src'))
617 dst = int(self.request.get('dst'))
618 q = PerformanceSamples.gql('WHERE interval=:1 and measured_at>=:2',
619 src, now - datetime.timedelta(seconds=dst))
620 samples = q.fetch(300)
621 # Tally each.
622 running_builds = {}
623 for s in samples:
624 srb = pickle.loads(s.running_builds)
625 for ss in srb:
626 old = running_builds.get(ss, {'count': 0, 'samples': 0})
627 running_builds[ss] = {
628 'count': old['count'] + srb[ss]['count'],
629 'samples': old['samples'] + srb[ss]['samples'],
630 }
631 # Store the total.
632 ps = PerformanceSamples()
633 ps.running_builds = pickle.dumps(running_builds)
634 ps.interval = dst
635 ps.measured_at = now
636 ps.put()
637
638
639 class TallyRunning(webapp.RequestHandler):
640 """Handle web requests to record a sample of builds in progress."""
641
642 def get(self):
643 """Handle get requests."""
644 now = datetime.datetime.now()
645 q = BuildResult.gql('WHERE end=null ORDER BY start DESC')
646 builds = q.fetch(300)
647 # Find set of unique builders.
648 builders = set()
649 for b in builds:
650 builders.add((b.master_url, b.builder_name))
651 # Add in tally for each builder.
652 running_builds = {}
653 for bldr in builders:
654 count = 0
655 for b in builds:
656 if b.master_url == bldr[0] and b.builder_name == bldr[1]:
657 count += 1
658 # Add an entry for this builder.
659 running_builds[bldr] = {
660 'count': count,
661 'samples': 1,
662 }
663 # Write to database.
664 ps = PerformanceSamples()
665 ps.running_builds = pickle.dumps(running_builds)
666 ps.interval = 60
667 ps.measured_at = now
668 ps.put()
669
670
671 class StartWork(webapp.RequestHandler):
672 """Handle web cron requests to initiate an update work cycle."""
673
674 def get(self):
675 """Handle get requests."""
676 # Run tasks for master.
677 for master_url in BUILDBOT_URLS:
678 taskqueue.Task(
679 url='/update_builders',
680 params={'master_url': master_url}).add(
681 queue_name='update-builders')
682 # Initiate updates on existing builds.
683 for t in range(0, 10):
684 taskqueue.Task(
685 url='/update_builds',
686 countdown=6*t).add(queue_name='update-builds')
687
688
689 application = webapp.WSGIApplication([
690 ('/', MainPage),
691 ('/start_work', StartWork),
692 ('/update_builders', UpdateBuilders),
693 ('/update_builder', UpdateBuilder),
694 ('/update_builds', UpdateBuilds),
695 ('/introduce_build', IntroduceBuild),
696 ('/tally_running', TallyRunning),
697 ('/tally_merge', TallyMerge),
698 ('/performance_raw', PerformanceRaw),
699 ('/list_builds', ListBuilds),
700 ('/build_info2', BuildInfo),
701 ('/build_info', StalePage),
702 ('/builder_dump', BuilderDump),
703 ], debug=True)
704
705
706 def main():
707 run_wsgi_app(application)
708
709
710 if __name__ == '__main__':
711 main()
OLDNEW
« no previous file with comments | « tryconsole/templates/status.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698