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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « tryconsole/templates/status.html ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tryconsole/tryconsole.py
===================================================================
--- tryconsole/tryconsole.py (revision 0)
+++ tryconsole/tryconsole.py (revision 0)
@@ -0,0 +1,711 @@
+#!/usr/bin/python
+# Copyright (c) 2009-2010 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""AJAX-y console for try jobs."""
+
+import datetime
+import logging
+import os
+import pickle
+import re
+import time
+from google.appengine.api import memcache
+from google.appengine.api import urlfetch
+from google.appengine.api.labs import taskqueue
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+from google.appengine.ext.webapp.util import run_wsgi_app
+
+
+BUILDBOT_URLS = [
+ 'http://build.chromium.org/buildbot/try-server',
+]
+HISTORY_SIZE = 50
+
+
+class BuildResult(db.Model):
+ """Entity for storing the current status of a single trybot build."""
+ # TODO(bradnelson): make these required?
+ master_url = db.StringProperty()
+ builder_name = db.StringProperty()
+ build_number = db.IntegerProperty()
+
+ slave_name = db.StringProperty()
+
+ raw_reason = db.StringProperty()
+ username = db.StringProperty()
+ patch_name = db.StringProperty()
+
+ revision = db.IntegerProperty()
+ got_revision = db.IntegerProperty()
+
+ account_name = db.StringProperty()
+
+ changed_at = db.DateTimeProperty()
+ start = db.DateTimeProperty()
+ end = db.DateTimeProperty()
+
+ steps_and_logfiles = db.BlobProperty()
+
+ eta_seconds = db.IntegerProperty()
+ page_built = db.DateTimeProperty()
+ last_checked = db.DateTimeProperty()
+
+
+class PerformanceSamples(db.Model):
+ """A Sample of builds running, used to generate graphs."""
+ interval = db.IntegerProperty()
+ running_builds = db.BlobProperty()
+ measured_at = db.DateTimeProperty()
+
+
+def GetBuilders(master_url):
+ """Given the URL of a trybot master, get a list of builders for that master.
+
+ Arguments:
+ master_url: the URL of the master.
+ Returns:
+ A list of strings containing the builder names.
+ """
+ # TODO(bradnelson): use the json interface in place of html scraping.
+ # Returned cached copy if we have one.
+ blist = memcache.get(master_url + '/builders')
+ if blist:
+ return blist
+ # Grab the webpage.
+ url = master_url + '/builders'
+ try:
+ result = urlfetch.fetch(url)
+ except urlfetch.DownloadError:
+ return []
+ if result.status_code != 200:
+ return []
+ # Pick out the proper list.
+ text = re.search('<ol>(.*)</ol>', result.content, re.DOTALL)
+ if not text:
+ return []
+ # Pick out the builder name.
+ blist = re.findall('<li><a href[^>]+>([^<]+)</a></li>', text.group(1))
+ # Add to cache.
+ memcache.add('/builders', blist, 1000)
+ return blist
+
+
+def GetBuilderInfo(master_url, builder_name):
+ """Given a trybot master and builder name, get information about a builder.
+
+ Arguments:
+ master_url: the URL of the master.
+ builder_name: the name of a builder on this master.
+ Returns:
+ A dict filled with a range of values that describe the state of this
+ builder. It contains things like which builds are currently running.
+ """
+ # TODO(bradnelson): use the json interface in place of html scraping.
+ # Returned cached copy if we have one.
+ key = '%s/builders/%s' % (master_url, builder_name)
+ info = memcache.get(key)
+ if info:
+ return info
+ # Prep it.
+ info = {
+ 'master_url': master_url,
+ 'name': builder_name,
+ 'currently_building': [],
+ 'recent_builds': [],
+ }
+ # Grab the webpage.
+ url = master_url + '/builders/' + builder_name
+ try:
+ result = urlfetch.fetch(url)
+ except urlfetch.DownloadError:
+ return info
+ if result.status_code != 200:
+ return info
+ # Pick out the currently building list.
+ text = re.search('<h2>Currently Building:</h2>[ \n\r]+<ul>(.*?)</ul>',
+ result.content, re.DOTALL)
+ if text:
+ # Pick out info about each one currently building.
+ builders = re.findall(r'<li>([^:]+): <a[^>]+>#([0-9]+)</a> '
+ r'ETA ([0-9]+)s \(([^)]+)\) \[([^]]+)\]</li>',
+ text.group(1))
+ for b in builders:
+ e = {
+ 'slave_name': str(b[0]),
+ 'build_number': int(b[1]),
+ 'eta_seconds': int(b[2]),
+ 'eta_time': str(b[3]),
+ 'stage': str(b[4]),
+ }
+ info['currently_building'].append(e)
+ # Pick out the recently built list.
+ text = re.search('<h2>Recent Builds:</h2>[ \n\r]+<ul>(.*?)</ul>',
+ result.content, re.DOTALL)
+ if text:
+ # Pick out info about each.
+ builders = re.findall(r'>#([0-9]+)</a>:', text.group(1))
+ for b in builders:
+ e = {
+ 'build_number': int(b),
+ }
+ info['recent_builds'].append(e)
+ # Find highest builder number.
+ build_nums = [0]
+ build_nums += [i['build_number'] for i in info['currently_building']]
+ build_nums += [i['build_number'] for i in info['recent_builds']]
+ info['latest'] = max(build_nums)
+ # Add to cache.
+ memcache.add(key, info, 1)
+ return info
+
+
+def CheckForBuild(master_url, builder_name, build_number):
+ """Check if a particular build exists.
+
+ Arguments:
+ master_url: the URL of the master.
+ builder_name: the name of a builder on this master.
+ build_number: integer builder number.
+ Returns:
+ True or False depending on if this build exists in the data-store.
+ """
+ q = db.GqlQuery('SELECT __key__ FROM BuildResult '
+ 'WHERE master_url=:1 '
+ 'AND builder_name=:2 '
+ 'AND build_number=:3',
+ master_url, builder_name, build_number)
+ b = q.fetch(1)
+ if not b or len(b) != 1:
+ return False
+ else:
+ return True
+
+
+def UpdateBuildResult(br):
+ """Update data-store information on a particular build.
+
+ Checks the master for fresh information on a given build.
+ Arguments:
+ br: A build result to update.
+ """
+ # TODO(bradnelson): use the json interface in place of html scraping.
+ # Advance time.
+ br.last_checked = datetime.datetime.now()
+ # Grab the webpage.
+ url = '%s/builders/%s/builds/%d' % (
+ br.master_url, br.builder_name, br.build_number)
+ try:
+ result = urlfetch.fetch(url)
+ except urlfetch.DownloadError:
+ br.put()
+ logging.debug('Download Error Updating %s | %s | %d',
+ br.master_url, br.builder_name, br.build_number)
+ return
+ # Handle 404 specially.
+ if result.status_code == 404:
+ # Put in an empty entry for expired ones.
+ # But only do it if we have nothing else.
+ if not CheckForBuild(br.master_url, br.builder_name, br.build_number):
+ br.end = datetime.datetime.fromordinal(1)
+ br.put()
+ if result.status_code != 200:
+ br.put()
+ logging.debug('Result Code %d Updating %s | %s | %d',
+ result.status_code,
+ br.master_url, br.builder_name, br.build_number)
+ return
+
+ # Pick out content.
+ content = result.content
+
+ # Pick out steps and logfiles.
+ text = re.search('<h2>Build In Progress</h2><div>'
+ r'ETA ([0-9]+)s \(([^)]+)\)</div>', content)
+ if text:
+ br.eta_seconds = int(text.group(1))
+ else:
+ br.eta_seconds = 0
+ # Pick out steps and logfiles.
+ text = re.search('<h2>Steps and Logfiles:</h2>[ \n\r]+'
+ '<ol>(.+?)</ol>[ \n\r]*<h2>',
+ content, re.DOTALL)
+ if text:
+ br.steps_and_logfiles = text.group(1)
+ # Pick out reason.
+ text = re.search('<h2>Reason:</h2>[\n\r]+([^\n\r].+?)[\n\r]+<h2>',
+ content, re.DOTALL)
+ if text:
+ br.raw_reason = text.group(1)
+ m = re.match("'([^:]+): ([^']+)' try job", br.raw_reason)
+ if m:
+ br.username = m.group(1)
+ br.patch_name = m.group(2)
+ # Pick out source stamp.
+ text = re.search('<h2>SourceStamp:</h2>[\n\r ]+<ul>(.+?)</ul>',
+ content, re.DOTALL)
+ if text:
+ m = re.search('<li>Revision: ([0-9]+)</li>', text.group(1))
+ if m:
+ br.revision = int(m.group(1))
+ m = re.search('<li>Got Revision: ([0-9]+)</li>', text.group(1))
+ if m:
+ br.got_revision = int(m.group(1))
+ # Pick out all changes.
+ text = re.search('<h2>All Changes</h2>[\n\r ]+<ol>(.+?)</ol>',
+ content, re.DOTALL)
+ if text:
+ # Pick out who changed it.
+ m = re.search('<p>Changed by: <b>(.+?)</b><br />', text.group(1))
+ if m:
+ br.account_name = m.group(1)
+ # Pick out when the change occured.
+ m = re.search('Changed at: <b>(.+?)</b><br />', text.group(1))
+ if m:
+ tm = time.strptime(m.group(1), '%a %d %b %Y %H:%M:%S')
+ br.changed_at = datetime.datetime(*(tm)[0:6])
+ # Pick out timing.
+ text = re.search('<h2>Timing</h2>[\n\r ]+<table>(.+?)</table>',
+ content, re.DOTALL)
+ if text:
+ m = re.search('<tr><td>Start</td><td>(.+?)</td></tr>', text.group(1))
+ if m:
+ tm = time.strptime(m.group(1), '%a %b %d %H:%M:%S %Y')
+ br.start = datetime.datetime(*(tm)[0:6])
+ m = re.search('<tr><td>End</td><td>(.+?)</td></tr>', text.group(1))
+ if m:
+ tm = time.strptime(m.group(1), '%a %b %d %H:%M:%S %Y')
+ br.end = datetime.datetime(*(tm)[0:6])
+ # Pick page built.
+ text = re.search('Page built: (.+?)[\n\r ]+</div>',
+ content, re.DOTALL)
+ if text:
+ tm = time.strptime(text.group(1), '%a %d %b %Y %H:%M:%S')
+ br.page_built = datetime.datetime(*(tm)[0:6])
+ # Store it.
+ br.put()
+ # Log it.
+ logging.debug('Updating %s | %s | %d',
+ br.master_url, br.builder_name, br.build_number)
+
+
+def IntroduceBuildResult(master_url, builder_name, build_number):
+ """Introduces information into the data-store on a particular build.
+
+ Checks the master for fresh information on a given build.
+ Arguments:
+ master_url: the URL of the master.
+ builder_name: the name of a builder on this master.
+ build_number: integer builder number.
+ """
+ # Get the current entry if any.
+ q = BuildResult.gql('WHERE master_url=:1 and '
+ 'builder_name=:2 and '
+ 'build_number=:3',
+ master_url, builder_name, build_number)
+ br = q.fetch(1)
+ if br:
+ br = br[0]
+ else:
+ br = BuildResult()
+ # Set what we know.
+ br.master_url = master_url
+ br.builder_name = builder_name
+ br.build_number = build_number
+ # Update it.
+ UpdateBuildResult(br)
+
+
+class UpdateBuilders(webapp.RequestHandler):
+ """Handles web requests to update all builders on a given trybot master."""
+
+ def post(self):
+ # Update info on the current set of builders.
+ master_url = self.request.get('master_url')
+ builders = GetBuilders(master_url)
+ for builder_name in builders:
+ taskqueue.Task(
+ url='/update_builder',
+ params={'master_url': master_url,
+ 'builder_name': builder_name}).add(
+ queue_name='update-builder')
+
+
+def GetLatestBuilderBuild(master_url, builder_name):
+ """Given a builder, find the latest build number for it.
+
+ Arguments:
+ master_url: the URL of the master.
+ builder_name: the name of a builder on this master.
+ Returns:
+ The integer value of the latest build number for this builder.
+ """
+ q = BuildResult.gql('WHERE master_url=:1 '
+ 'AND builder_name=:2 '
+ 'ORDER BY build_number DESC',
+ master_url, builder_name)
+ latest = q.fetch(1)
+ if not latest or len(latest) != 1:
+ return 0
+ else:
+ return latest[0].build_number
+
+
+class UpdateBuilder(webapp.RequestHandler):
+ """Handle a web request to poll the status of a given builder."""
+
+ def post(self):
+ """Handle post requests."""
+ # Update info on one builder.
+ master_url = self.request.get('master_url')
+ builder_name = self.request.get('builder_name')
+ # Get latest build known to builder.
+ info = GetBuilderInfo(master_url, builder_name)
+ latest_known = info.get('latest', 1)
+ # Get latest build in the datastore.
+ latest_stored = GetLatestBuilderBuild(master_url, builder_name)
+ # We're done if they match,.
+ if latest_stored >= latest_known: return
+ # Add a task to introduce up to that build.
+ taskqueue.Task(
+ url='/introduce_build',
+ params={'master_url': master_url,
+ 'builder_name': builder_name,
+ 'begin': str(latest_stored + 1),
+ 'end': str(latest_known)}).add(
+ queue_name='introduce-build')
+
+
+class IntroduceBuild(webapp.RequestHandler):
+ """Handles a web request to introduce a new build to the datastore."""
+
+ def get(self):
+ """Handle get requests as post requests."""
+ self.post()
+
+ def post(self):
+ """Handle post requests."""
+ # Update info on one builder.
+ master_url = self.request.get('master_url')
+ builder_name = self.request.get('builder_name')
+ begin = int(self.request.get('begin'))
+ end = int(self.request.get('end'))
+ # Done if begin > end.
+ if begin > end: return
+ # Update one past current one.
+ IntroduceBuildResult(master_url, builder_name, begin)
+ # Log it.
+ logging.debug('Introducing %s | %s | %d',
+ master_url, builder_name, begin)
+ # Advance to the next one.
+ begin += 1
+ # Done if begin > end.
+ if begin > end: return
+ # Trigger self again.
+ taskqueue.Task(
+ url='/introduce_build',
+ params={'master_url': master_url,
+ 'builder_name': builder_name,
+ 'begin': str(begin),
+ 'end': str(end)}).add(
+ queue_name='introduce-build')
+
+
+class UpdateBuilds(webapp.RequestHandler):
+ """Handle a web request to update the most stale build still in progress."""
+
+ def post(self):
+ """Handle post requests."""
+ # Update info on one build.
+ q = BuildResult.gql('WHERE end=null ORDER BY last_checked')
+ stale = q.fetch(1)
+ if not stale:
+ return
+ for br in stale:
+ UpdateBuildResult(br)
+
+
+def GetBuildListFromDB(count):
+ """Fetch a list of the keys for the most recent builds.
+
+ Arguments:
+ count: a limit on the number of builds to fetch.
+ Returns:
+ A list of database keys for the latest builds.
+ """
+ q = db.GqlQuery('SELECT __key__ FROM BuildResult ORDER BY start DESC')
+ builds = q.fetch(count)
+ # List out the keys for these builds.
+ info = ''
+ for b in builds:
+ info += '%s\n' % b
+ return info
+
+
+def GetBuildList(count):
+ """Fetch a list of the keys for the most recent builds, cached.
+
+ Cache recent builds in the memcache, to reduce load on the data-store.
+ Arguments:
+ count: a limit on the number of builds to fetch.
+ Returns:
+ A list of database keys for the latest builds.
+ """
+ key = 'listbuilds//%d' % count
+ info = memcache.get(key)
+ if not info:
+ info = GetBuildListFromDB(count)
+ memcache.set(key, info, 20)
+ return info
+
+
+class ListBuilds(webapp.RequestHandler):
+ """Handle a web request for the latest builds."""
+
+ def get(self):
+ """Handle get requests."""
+ self.response.headers['Content-Type'] = 'text/plain'
+ # Get a different number of builds based on if this is the initial set.
+ if self.request.get('initial', 0):
+ info = GetBuildList(300)
+ else:
+ info = GetBuildList(50)
+ # Write it out.
+ self.response.out.write(info)
+
+
+class BuilderDump(webapp.RequestHandler):
+ """Handle a web request for information on a particular builder."""
+
+ def get(self):
+ """Handle get requests."""
+ self.response.headers['Content-Type'] = 'text/plain'
+ q = BuildResult.gql('WHERE builder_name=:1 '
+ 'and build_number >= :2 '
+ 'ORDER BY build_number',
+ self.request.get('builder'),
+ int(self.request.get('start')))
+ builds = q.fetch(30)
+ for i in builds:
+ self.response.out.write('%d\n' % i.build_number)
+
+
+class BuildInfo(webapp.RequestHandler):
+ """Handle a web request for information on a particular build."""
+
+ def get(self):
+ """Handle get requests."""
+ self.response.headers['Content-Type'] = 'text/plain'
+ build_id = str(self.request.get('id'))
+ # Decide the key to use for the memory cache.
+ key = 'buildinfo3//' + build_id
+ # Try to get a cached copy.
+ cinfo = memcache.get(key)
+ if cinfo:
+ cache_on_client = cinfo[0]
+ info = cinfo[1]
+ else:
+ # Load from datastore.
+ br = BuildResult.get(build_id)
+ if not br: return
+ # Write out info on this build.
+ info = ''
+ info += ('master_url %s\n' % br.master_url)
+ info += ('builder_name %s\n' % br.builder_name)
+ info += ('build_number %s\n' % br.build_number)
+ info += ('slave_name %s\n' % br.slave_name)
+ info += ('raw_reason %s\n' % br.raw_reason)
+ info += ('username %s\n' % br.username)
+ info += ('patch_name %s\n' % br.patch_name)
+ info += ('got_revision %s\n' % br.got_revision)
+ info += ('account_name %s\n' % br.account_name)
+ info += ('changed_at %s\n' % br.changed_at)
+ info += ('start %s\n' % br.start)
+ info += ('end %s\n' % br.end)
+ info += ('eta_seconds %s\n' % br.eta_seconds)
+ info += ('page_built %s\n' % br.page_built)
+ info += ('last_checked %s\n' % br.last_checked)
+ logs = re.sub('[\r\n\t]', ' ', br.steps_and_logfiles)
+ info += ('steps_and_logfiles %s\n' % logs)
+ # Only allow caching on client if its done building.
+ cache_on_client = br.end is not None
+ if cache_on_client:
+ timeout = 0
+ else:
+ timeout = 15
+ memcache.set(key, (cache_on_client, info), time=timeout)
+ # Allow the client to cache it for a day, assumming its done.
+ if cache_on_client:
+ self.response.headers['Cache-Control'] = 'public, max-age=86400'
+ # Write out the info.
+ self.response.out.write(info)
+
+
+class StalePage(webapp.RequestHandler):
+ """Return a long cached value for requests to historical stale pages."""
+
+ def get(self):
+ """Handle get requests."""
+ self.response.headers['Content-Type'] = 'text/plain'
+ self.response.headers['Cache-Control'] = 'public, max-age=86400'
+
+
+def ChromeFrameMe(handler):
+ """Handle web requests to instruct users to install ChromeFrame.
+
+ Arguments:
+ handler: the current webapp.RequestHandler.
+ Returns:
+ True or False indicating success.
+ """
+ agent = handler.request.headers.get('USER_AGENT', '')
+ if agent.find('MSIE') >= 0 and agent.find('chromeframe') < 0:
+ path = os.path.join(os.path.dirname(__file__),
+ 'templates/chrome_frame.html')
+ handler.response.out.write(template.render(path, {}))
+ return True
+ return False
+
+
+class MainPage(webapp.RequestHandler):
+ """Handle web requests for the main tryconsole page."""
+
+ def get(self):
+ """Handle get requests."""
+ if ChromeFrameMe(self): return
+ path = os.path.join(os.path.dirname(__file__), 'templates/status.html')
+ self.response.out.write(template.render(path, {}))
+
+
+class PerformanceRaw(webapp.RequestHandler):
+ """Handle web requests for graphed performance data."""
+
+ def get(self):
+ """Handle get requests."""
+ now = datetime.datetime.now()
+ self.response.headers['Content-Type'] = 'text/plain'
+ interval = int(self.request.get('interval'))
+ start = int(self.request.get('start'))
+ stop = int(self.request.get('stop'))
+ q = PerformanceSamples.gql('WHERE interval=:1 and '
+ 'measured_at>=:2 and '
+ 'measured_at<=:3 '
+ 'ORDER BY measured_at',
+ interval,
+ now - datetime.timedelta(seconds=start),
+ now - datetime.timedelta(seconds=stop))
+ samples = q.fetch(1000)
+ for s in samples:
+ measured_at = int(time.mktime(s.measured_at.timetuple()))
+ srb = pickle.loads(s.running_builds)
+ for b in srb:
+ self.response.out.write(
+ '%s|%s|%d|%d|%d\n' % (b[0], b[1], measured_at,
+ srb[b]['count'], srb[b]['samples']))
+
+
+class TallyMerge(webapp.RequestHandler):
+ """Handle web requests to generate overview performance data."""
+
+ def get(self):
+ """Handle get requests."""
+ now = datetime.datetime.now()
+ src = int(self.request.get('src'))
+ dst = int(self.request.get('dst'))
+ q = PerformanceSamples.gql('WHERE interval=:1 and measured_at>=:2',
+ src, now - datetime.timedelta(seconds=dst))
+ samples = q.fetch(300)
+ # Tally each.
+ running_builds = {}
+ for s in samples:
+ srb = pickle.loads(s.running_builds)
+ for ss in srb:
+ old = running_builds.get(ss, {'count': 0, 'samples': 0})
+ running_builds[ss] = {
+ 'count': old['count'] + srb[ss]['count'],
+ 'samples': old['samples'] + srb[ss]['samples'],
+ }
+ # Store the total.
+ ps = PerformanceSamples()
+ ps.running_builds = pickle.dumps(running_builds)
+ ps.interval = dst
+ ps.measured_at = now
+ ps.put()
+
+
+class TallyRunning(webapp.RequestHandler):
+ """Handle web requests to record a sample of builds in progress."""
+
+ def get(self):
+ """Handle get requests."""
+ now = datetime.datetime.now()
+ q = BuildResult.gql('WHERE end=null ORDER BY start DESC')
+ builds = q.fetch(300)
+ # Find set of unique builders.
+ builders = set()
+ for b in builds:
+ builders.add((b.master_url, b.builder_name))
+ # Add in tally for each builder.
+ running_builds = {}
+ for bldr in builders:
+ count = 0
+ for b in builds:
+ if b.master_url == bldr[0] and b.builder_name == bldr[1]:
+ count += 1
+ # Add an entry for this builder.
+ running_builds[bldr] = {
+ 'count': count,
+ 'samples': 1,
+ }
+ # Write to database.
+ ps = PerformanceSamples()
+ ps.running_builds = pickle.dumps(running_builds)
+ ps.interval = 60
+ ps.measured_at = now
+ ps.put()
+
+
+class StartWork(webapp.RequestHandler):
+ """Handle web cron requests to initiate an update work cycle."""
+
+ def get(self):
+ """Handle get requests."""
+ # Run tasks for master.
+ for master_url in BUILDBOT_URLS:
+ taskqueue.Task(
+ url='/update_builders',
+ params={'master_url': master_url}).add(
+ queue_name='update-builders')
+ # Initiate updates on existing builds.
+ for t in range(0, 10):
+ taskqueue.Task(
+ url='/update_builds',
+ countdown=6*t).add(queue_name='update-builds')
+
+
+application = webapp.WSGIApplication([
+ ('/', MainPage),
+ ('/start_work', StartWork),
+ ('/update_builders', UpdateBuilders),
+ ('/update_builder', UpdateBuilder),
+ ('/update_builds', UpdateBuilds),
+ ('/introduce_build', IntroduceBuild),
+ ('/tally_running', TallyRunning),
+ ('/tally_merge', TallyMerge),
+ ('/performance_raw', PerformanceRaw),
+ ('/list_builds', ListBuilds),
+ ('/build_info2', BuildInfo),
+ ('/build_info', StalePage),
+ ('/builder_dump', BuilderDump),
+], debug=True)
+
+
+def main():
+ run_wsgi_app(application)
+
+
+if __name__ == '__main__':
+ main()
« 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